Repository: CloudWise-OpenSource/OMP Branch: main Commit: 40afb5a4b9d1 Files: 659 Total size: 6.1 MB Directory structure: gitextract_kwjdxdir/ ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── UpdateLog.md ├── component/ │ ├── .gitkeep │ ├── alertmanager/ │ │ └── .gitkeep │ ├── grafana/ │ │ └── .gitkeep │ ├── loki/ │ │ └── .gitkeep │ └── prometheus/ │ └── .gitkeep ├── config/ │ ├── omp.yaml │ ├── private_key.pem │ ├── product.yaml │ └── salt/ │ ├── master │ ├── minion │ ├── minion.d/ │ │ └── _schedule.conf │ └── minion.template ├── data/ │ ├── .gitkeep │ └── inspection_file/ │ └── .gitkeep ├── doc/ │ ├── app_publish.md │ └── changelogs.md ├── logs/ │ └── .gitkeep ├── omp_server/ │ ├── app_store/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── app_store_filters.py │ │ ├── app_store_serializers.py │ │ ├── apps.py │ │ ├── cmd_install_utils.py │ │ ├── deploy_mode_utils/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── even_num.py │ │ │ ├── mysql.py │ │ │ ├── normal.py │ │ │ ├── odd_num.py │ │ │ ├── rocketmq.py │ │ │ └── tengine.py │ │ ├── deploy_role_utils/ │ │ │ ├── __init__.py │ │ │ ├── hadoop.py │ │ │ ├── mysql.py │ │ │ └── redis.py │ │ ├── high_availability_utils/ │ │ │ ├── __init__.py │ │ │ └── hadoop.py │ │ ├── install_exec.py │ │ ├── install_executor.py │ │ ├── install_utils.py │ │ ├── new_install_serializers.py │ │ ├── new_install_utils.py │ │ ├── new_install_view.py │ │ ├── post_install_utils/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── nacos.py │ │ │ └── tengine.py │ │ ├── service_splitting.py │ │ ├── tasks.py │ │ ├── tmp_exec_back_task.py │ │ ├── upload_task.py │ │ ├── urls.py │ │ ├── views.py │ │ └── views_for_install.py │ ├── backups/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── backup_service.py │ │ ├── backups_serializers.py │ │ ├── backups_utils.py │ │ ├── migrations/ │ │ │ └── __init__.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── db_models/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations/ │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20211202_1830.py │ │ │ ├── 0003_host_init_status.py │ │ │ ├── 0004_auto_20211203_1617.py │ │ │ ├── 0005_auto_20211206_1723.py │ │ │ ├── 0005_update_init_status.py │ │ │ ├── 0006_merge_20211206_1833.py │ │ │ ├── 0007_deploymentplan.py │ │ │ ├── 0008_service_vip.py │ │ │ ├── 0009_auto_20211228_1603.py │ │ │ ├── 0010_auto_20220114_1830.py │ │ │ ├── 0010_backuphistory_backupsetting.py │ │ │ ├── 0011_auto_20220112_1607.py │ │ │ ├── 0012_auto_20220112_1653.py │ │ │ ├── 0013_merge_20220114_1838.py │ │ │ ├── 0014_auto_20220121_1616.py │ │ │ ├── 0015_executionrecord.py │ │ │ ├── 0016_auto_20220125_1800.py │ │ │ ├── 0017_selfhealinghistory_selfhealingsetting.py │ │ │ ├── 0018_userloginlog_request_result.py │ │ │ ├── 0019_toolexecutedetailhistory_toolexecutemainhistory_toolinfo_uploadfilehistory.py │ │ │ ├── 0020_init_tools.py │ │ │ ├── 0021_customscript.py │ │ │ ├── 0022_alertrule_rule.py │ │ │ ├── 0023_auto_20220225_1747.py │ │ │ ├── 0024_auto_20220226_1300.py │ │ │ ├── 0025_alertrule_forbidden.py │ │ │ ├── 0026_alertrule_hash_data.py │ │ │ ├── 0026_auto_20220303_1527.py │ │ │ ├── 0027_merge_20220304_2000.py │ │ │ ├── 0028_auto_20220304_2001.py │ │ │ ├── 0029_auto_20230110_1739.py │ │ │ ├── 0030_auto_20230711_1739.py │ │ │ ├── 0031_auto_20230921_1128.py │ │ │ └── __init__.py │ │ ├── mixins.py │ │ ├── models/ │ │ │ ├── __init__.py │ │ │ ├── backup.py │ │ │ ├── custom_metric.py │ │ │ ├── email.py │ │ │ ├── env.py │ │ │ ├── execution.py │ │ │ ├── host.py │ │ │ ├── inspection.py │ │ │ ├── install.py │ │ │ ├── monitor.py │ │ │ ├── product.py │ │ │ ├── self_heal.py │ │ │ ├── service.py │ │ │ ├── threshold.py │ │ │ ├── tool.py │ │ │ ├── upgrade.py │ │ │ ├── upload.py │ │ │ └── user.py │ │ ├── receivers/ │ │ │ ├── __init__.py │ │ │ ├── execution_record.py │ │ │ └── service.py │ │ └── signals/ │ │ └── __init__.py │ ├── dev_code.md │ ├── dev_requirement.txt │ ├── hosts/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── hosts_filters.py │ │ ├── hosts_serializers.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── inspection/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── filters.py │ │ ├── get_prometheus_risk_data.py │ │ ├── get_service_topology.py │ │ ├── inspection_utils.py │ │ ├── joint_json_report.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ ├── omp_server/ │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── celery.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── promemonitor/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── alert_util.py │ │ ├── alertmanager.py │ │ ├── apps.py │ │ ├── custom_script_serializers.py │ │ ├── custom_script_views.py │ │ ├── grafana_url.py │ │ ├── grafana_views.py │ │ ├── promemonitor_filters.py │ │ ├── promemonitor_serializers.py │ │ ├── prometheus.py │ │ ├── prometheus_utils.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── service_upgrade/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── filters.py │ │ ├── handler/ │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── rollback_handler.py │ │ │ └── upgrade_handler.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── update_data_json.py │ │ ├── urls.py │ │ └── views.py │ ├── services/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── app_check/ │ │ │ ├── __init__.py │ │ │ ├── conf_check.py │ │ │ └── manage_ser_exec.py │ │ ├── apps.py │ │ ├── permission.py │ │ ├── self_heal_filter.py │ │ ├── self_heal_serializers.py │ │ ├── self_heal_util.py │ │ ├── self_heal_view.py │ │ ├── self_healing.py │ │ ├── services_filters.py │ │ ├── services_serializers.py │ │ ├── tasks.py │ │ ├── urls.py │ │ └── views.py │ ├── tests/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── mixin.py │ │ ├── test_app_store/ │ │ │ ├── __init__.py │ │ │ ├── install_data_source.py │ │ │ ├── make_install_fake_data.py │ │ │ ├── test_app_check.py │ │ │ ├── test_app_store.py │ │ │ ├── test_app_store_install.py │ │ │ ├── test_app_store_upload.py │ │ │ ├── test_execute_package_scan.py │ │ │ ├── test_get_application_template.py │ │ │ ├── test_install_executor.py │ │ │ ├── test_new_install.py │ │ │ └── test_upload_package.py │ │ ├── test_hosts/ │ │ │ ├── __init__.py │ │ │ ├── test_celery_tasks.py │ │ │ └── test_hosts.py │ │ ├── test_inspection/ │ │ │ ├── __init__.py │ │ │ ├── inspection_mixin.py │ │ │ ├── test_crontab.py │ │ │ ├── test_history.py │ │ │ ├── test_inspection_email.py │ │ │ └── test_report.py │ │ ├── test_promemonitor/ │ │ │ ├── __init__.py │ │ │ ├── test_alert.py │ │ │ ├── test_alertmanager.py │ │ │ ├── test_celery_tasks.py │ │ │ ├── test_email_config.py │ │ │ ├── test_global_maintain.py │ │ │ ├── test_grafana_url.py │ │ │ ├── test_grafana_views.py │ │ │ ├── test_instance_name_list.py │ │ │ ├── test_instrument_panel.py │ │ │ ├── test_monitor_agent_restart.py │ │ │ ├── test_promemonitor_url.py │ │ │ ├── test_prometheus.py │ │ │ ├── test_prometheus_utils.py │ │ │ ├── test_receive_alert.py │ │ │ └── test_threshold_rw.py │ │ ├── test_services/ │ │ │ ├── __init__.py │ │ │ ├── test_service_actions.py │ │ │ └── test_services.py │ │ ├── test_users/ │ │ │ ├── __init__.py │ │ │ ├── test_login.py │ │ │ └── test_users.py │ │ └── test_utils/ │ │ ├── __init__.py │ │ ├── test_agent_util.py │ │ ├── test_crontab_utils.py │ │ ├── test_crypto.py │ │ ├── test_monitor_agent.py │ │ ├── test_public_utils.py │ │ ├── test_salt_client.py │ │ └── test_ssh.py │ ├── tool/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── find_tools.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── tests.py │ │ ├── tool_filters.py │ │ ├── urls.py │ │ └── views.py │ ├── users/ │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── urls.py │ │ ├── users_filters.py │ │ ├── users_serializers.py │ │ └── views.py │ └── utils/ │ ├── __init__.py │ ├── common/ │ │ ├── __init__.py │ │ ├── exceptions.py │ │ ├── paginations.py │ │ ├── serializers.py │ │ ├── urls.py │ │ ├── validators.py │ │ └── views.py │ ├── exception_handler.py │ ├── middleware_handler.py │ ├── parse_config.py │ ├── plugin/ │ │ ├── __init__.py │ │ ├── agent_util.py │ │ ├── captcha/ │ │ │ ├── __init__.py │ │ │ └── captcha.py │ │ ├── crontab_utils.py │ │ ├── crypto.py │ │ ├── install_ntpdate.py │ │ ├── monitor_agent.py │ │ ├── public_utils.py │ │ ├── salt_client.py │ │ ├── send_email.py │ │ ├── ssh.py │ │ └── synch_grafana.py │ ├── prometheus/ │ │ ├── __init__.py │ │ ├── create_html_tar.py │ │ ├── prometheus.py │ │ ├── target_host.py │ │ ├── target_service.py │ │ ├── target_service_arangodb.py │ │ ├── target_service_beanstalk.py │ │ ├── target_service_clickhouse.py │ │ ├── target_service_elasticsearch.py │ │ ├── target_service_flink.py │ │ ├── target_service_func.py │ │ ├── target_service_gotty.py │ │ ├── target_service_grafana.py │ │ ├── target_service_hadoop.py │ │ ├── target_service_httpd.py │ │ ├── target_service_ignite.py │ │ ├── target_service_jvm_base.py │ │ ├── target_service_kafka.py │ │ ├── target_service_mysql.py │ │ ├── target_service_nacos.py │ │ ├── target_service_ntpd.py │ │ ├── target_service_postgresql.py │ │ ├── target_service_prometheus.py │ │ ├── target_service_redis.py │ │ ├── target_service_rocketmq.py │ │ ├── target_service_tengine.py │ │ ├── target_service_zookeeper.py │ │ ├── thread.py │ │ ├── update_threshold.py │ │ └── utils.py │ └── response_handler.py ├── omp_web/ │ ├── README.md │ ├── config-overrides.js │ ├── package.json │ ├── public/ │ │ ├── index.html │ │ └── pubKey.json │ ├── src/ │ │ ├── App.js │ │ ├── components/ │ │ │ ├── CustomBreadcrumb/ │ │ │ │ ├── index.js │ │ │ │ ├── index.module.less │ │ │ │ └── store/ │ │ │ │ ├── actionsCreators.js │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ └── reduer.js │ │ │ ├── OmpButton/ │ │ │ │ └── index.js │ │ │ ├── OmpCollapseWrapper/ │ │ │ │ ├── index.js │ │ │ │ ├── index.module.less │ │ │ │ └── indexOld.js │ │ │ ├── OmpContentNav/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpContentWrapper/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpDatePicker/ │ │ │ │ └── index.js │ │ │ ├── OmpDrawer/ │ │ │ │ └── index.js │ │ │ ├── OmpIframe/ │ │ │ │ └── index.js │ │ │ ├── OmpMaintenanceModal/ │ │ │ │ └── index.js │ │ │ ├── OmpMessageModal/ │ │ │ │ └── index.js │ │ │ ├── OmpModal/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpOperationWrapper/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpProgress/ │ │ │ │ └── index.js │ │ │ ├── OmpSelect/ │ │ │ │ └── index.js │ │ │ ├── OmpStateBlock/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpTable/ │ │ │ │ ├── components/ │ │ │ │ │ └── OmpTableFilter.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── OmpToolTip/ │ │ │ │ └── index.js │ │ │ └── index.js │ │ ├── config/ │ │ │ ├── requestApi.js │ │ │ └── router.config.js │ │ ├── index.js │ │ ├── layouts/ │ │ │ ├── container/ │ │ │ │ └── container.js │ │ │ ├── index.js │ │ │ ├── index.module.less │ │ │ ├── indexOld.js │ │ │ └── store/ │ │ │ ├── actionsCreators.js │ │ │ ├── constants.js │ │ │ ├── index.js │ │ │ └── reduer.js │ │ ├── pages/ │ │ │ ├── AlarmLog/ │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ └── index.js │ │ │ ├── AppStore/ │ │ │ │ ├── config/ │ │ │ │ │ ├── ApplicationInstallation.js │ │ │ │ │ ├── BatchInstallationModal.js │ │ │ │ │ ├── ComponentInstallation.js │ │ │ │ │ ├── DeleteServerModal.js │ │ │ │ │ ├── GetService.js │ │ │ │ │ ├── GetServiceModal.js │ │ │ │ │ ├── Installation/ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ ├── BasicInfoItem/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── DependentinfoItem/ │ │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ │ ├── DeployInstanceRow.js │ │ │ │ │ │ │ │ │ ├── DeployNumRow.js │ │ │ │ │ │ │ │ │ ├── DeployRow.js │ │ │ │ │ │ │ │ │ ├── JdkRow.js │ │ │ │ │ │ │ │ │ ├── RenderArr.js │ │ │ │ │ │ │ │ │ └── RenderNum.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── InstallInfoItem/ │ │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ │ └── InstallDetail.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── ServiceConfigItem/ │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ ├── ServiceDistributionItem/ │ │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ │ └── HasInstallService.js │ │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ │ └── index.module.less │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ ├── index.module.less │ │ │ │ │ │ ├── steps/ │ │ │ │ │ │ │ ├── Step1.js │ │ │ │ │ │ │ ├── Step2.js │ │ │ │ │ │ │ ├── Step3.js │ │ │ │ │ │ │ └── Step4.js │ │ │ │ │ │ └── store/ │ │ │ │ │ │ ├── actionsCreators.js │ │ │ │ │ │ ├── constants.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── reduer.js │ │ │ │ │ ├── ReleaseModal.js │ │ │ │ │ ├── Rollback/ │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ ├── RollbackDetail.js │ │ │ │ │ │ │ │ └── RollbackInfoItem.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.module.less │ │ │ │ │ ├── ScanServerModal.js │ │ │ │ │ ├── ServiceRollbackModal.js │ │ │ │ │ ├── ServiceUpgradeModal.js │ │ │ │ │ ├── Upgrade/ │ │ │ │ │ │ ├── content/ │ │ │ │ │ │ │ ├── component/ │ │ │ │ │ │ │ │ ├── UpgradeDetail.js │ │ │ │ │ │ │ │ └── UpgradeInfoItem.js │ │ │ │ │ │ │ └── index.js │ │ │ │ │ │ ├── index.js │ │ │ │ │ │ └── index.module.less │ │ │ │ │ ├── card.js │ │ │ │ │ ├── component/ │ │ │ │ │ │ └── RenderComDependence.js │ │ │ │ │ ├── detail.js │ │ │ │ │ ├── img.js │ │ │ │ │ └── index.module.less │ │ │ │ ├── index.js │ │ │ │ ├── index.module.less │ │ │ │ └── store/ │ │ │ │ ├── actionsCreators.js │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ └── reduer.js │ │ │ ├── BackupRecords/ │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── BackupStrategy/ │ │ │ │ ├── CustomModal.js │ │ │ │ ├── StrategyModal.js │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── DeploymentPlan/ │ │ │ │ ├── config/ │ │ │ │ │ ├── columns.js │ │ │ │ │ └── models.js │ │ │ │ └── index.js │ │ │ ├── EmailSettings/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── ExceptionList/ │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ └── index.js │ │ │ ├── HomePage/ │ │ │ │ ├── index.js │ │ │ │ ├── index.module.less │ │ │ │ └── warningList.js │ │ │ ├── InstallationRecord/ │ │ │ │ ├── config/ │ │ │ │ │ └── ServiceUpgradeModal.js │ │ │ │ ├── index.js │ │ │ │ ├── indexOld.js │ │ │ │ └── tabs/ │ │ │ │ ├── installation.js │ │ │ │ ├── rollback.js │ │ │ │ └── upgrade.js │ │ │ ├── Login/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── LoginLog/ │ │ │ │ └── index.js │ │ │ ├── MachineManagement/ │ │ │ │ ├── config/ │ │ │ │ │ ├── columns.js │ │ │ │ │ └── modals.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── MonitoringSettings/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── PatrolInspectionRecord/ │ │ │ │ ├── config/ │ │ │ │ │ ├── columns.js │ │ │ │ │ ├── detail.js │ │ │ │ │ └── index.css │ │ │ │ └── index.js │ │ │ ├── PatrolStrategy/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── RuleCenter/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── RuleExtend/ │ │ │ │ └── index.js │ │ │ ├── RuleIndicator/ │ │ │ │ └── index.js │ │ │ ├── SelfHealingRecord/ │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ └── index.js │ │ │ ├── SelfHealingStrategy/ │ │ │ │ ├── StrategyModal.js │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── ServiceManagement/ │ │ │ │ ├── config/ │ │ │ │ │ └── columns.js │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── SystemLog/ │ │ │ │ └── index.js │ │ │ ├── SystemManagement/ │ │ │ │ ├── index.js │ │ │ │ ├── index.module.less │ │ │ │ └── store/ │ │ │ │ ├── actionsCreators.js │ │ │ │ ├── constants.js │ │ │ │ ├── index.js │ │ │ │ └── reduer.js │ │ │ ├── TaskRecord/ │ │ │ │ └── index.js │ │ │ ├── ToolExecution/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── ToolExecutionResults/ │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ ├── ToolManagement/ │ │ │ │ ├── config/ │ │ │ │ │ ├── card.js │ │ │ │ │ └── index.module.less │ │ │ │ ├── detail/ │ │ │ │ │ ├── Readme.js │ │ │ │ │ ├── index.js │ │ │ │ │ └── index.module.less │ │ │ │ ├── index.js │ │ │ │ └── index.module.less │ │ │ └── UserManagement/ │ │ │ └── index.js │ │ ├── react-app-env.d.ts │ │ ├── router.js │ │ ├── store_redux/ │ │ │ ├── reducer.js │ │ │ └── reduxStore.js │ │ └── utils/ │ │ ├── index.module.less │ │ ├── request.js │ │ └── utils.js │ └── tsconfig.json ├── package_hub/ │ ├── .gitkeep │ ├── _modules/ │ │ ├── __init__.py │ │ ├── arangodb_check.py │ │ ├── beanstalkd_check.py │ │ ├── clickhouse_check.py │ │ ├── elasticsearch_check.py │ │ ├── flink_check.py │ │ ├── get_agent_info.py │ │ ├── gotty_check.py │ │ ├── grafana_check.py │ │ ├── hadoop_check.py │ │ ├── host_check.py │ │ ├── httpd_check.py │ │ ├── ignite_check.py │ │ ├── init_host.py │ │ ├── inspection_common.py │ │ ├── kafka_check.py │ │ ├── minio_check.py │ │ ├── mysql_bak.sh.tmp │ │ ├── mysql_check.py │ │ ├── nacos_check.py │ │ ├── ntpd_check.py │ │ ├── postgreSql_bak.sh.tmp │ │ ├── postgresql_check.py │ │ ├── prometheus_check.py │ │ ├── rocketmq_check.py │ │ ├── tengine_check.py │ │ ├── tomcat_check.py │ │ └── zookeeper_check.py │ ├── back_end_verified/ │ │ └── .gitkeep │ ├── custom_scripts/ │ │ ├── .gitkeep │ │ └── template.py │ ├── data_files/ │ │ └── .gitkeep │ ├── front_end_verified/ │ │ └── .gitkeep │ ├── grafana_dashboard_json/ │ │ ├── 21-rediscluster-xin-xi-mian-ban.json │ │ ├── 1-zhu-ji-xin-xi-mian-ban.json │ │ ├── 10-ignite-xin-xi-mian-ban.json │ │ ├── 11-kafka-xin-xi-mian-ban.json │ │ ├── 12-mysql-xin-xi-mian-ban.json │ │ ├── 13-nacos-xin-xi-mian-ban.json │ │ ├── 14-postgresql-xin-xi-mian-ban.json │ │ ├── 15-redis-xin-xi-mian-ban.json │ │ ├── 16-tengine-nginx-xin-xi-mian-ban.json │ │ ├── 17-zookeeper-xin-xi-mian-ban.json │ │ ├── 18-exporter-status.json │ │ ├── 19-jvm-xin-xi-mian-ban.json │ │ ├── 2-fu-wu-zhuang-tai-xin-xi-mian-ban.json │ │ ├── 20-clickhousecluster-xin-xi-mian-ban.json │ │ ├── 22-mysqlcluster-xin-xi-mian-ban.json │ │ ├── 23-tenginecluster-nginx-xin-xi-mian-ban.json │ │ ├── 24-victoriametrics-xin-xi-mian-ban.json │ │ ├── 25-rocketmq-xin-xi-mian-ban.json │ │ ├── 3-mian-ban-lie-biao.json │ │ ├── 4-app-logs.json │ │ ├── 5-arangodb-xin-xi-mian-ban.json │ │ ├── 6-beanstalkdxin-xi-mian-ban.json │ │ ├── 7-clickhouse-xin-xi-mian-ban.json │ │ ├── 8-elasticsearch-xin-xi-mian-ban.json │ │ └── 9-httpd-xin-xi-mian-ban.json │ ├── openssl_upgrade/ │ │ └── upgrade_ssl.sh │ ├── prometheus_rules_template/ │ │ ├── exporter_status_rule.yml │ │ ├── node_data_rule.yml │ │ ├── node_rule.yml │ │ └── service_status_rule.yml │ ├── reactor/ │ │ ├── auth.sls │ │ ├── start.sls │ │ └── stop.sls │ ├── runners/ │ │ ├── agent_start.py │ │ └── agent_stop.py │ ├── template/ │ │ ├── app_publish_readme.md │ │ ├── deployment.xlsx │ │ ├── import_hosts_template.xlsx │ │ ├── inspection_html/ │ │ │ ├── asset-manifest.json │ │ │ ├── index.html │ │ │ └── static/ │ │ │ ├── css/ │ │ │ │ ├── 2.8ca66de9.chunk.css │ │ │ │ └── main.041ca26a.chunk.css │ │ │ ├── js/ │ │ │ │ ├── 2.0ca9bd94.chunk.js │ │ │ │ ├── 2.0ca9bd94.chunk.js.LICENSE.txt │ │ │ │ ├── main.e4ade54a.chunk.js │ │ │ │ └── runtime-main.da7bcbe2.js │ │ │ └── media/ │ │ │ ├── index.02867153.less │ │ │ ├── index.2041a1d4.less │ │ │ ├── index.2f186d27.less │ │ │ ├── index.32dc937e.less │ │ │ ├── index.383af9c4.less │ │ │ ├── index.51825487.less │ │ │ ├── index.60c6e3ea.less │ │ │ ├── index.67101e84.less │ │ │ ├── index.68b48da1.less │ │ │ ├── index.73987a8f.less │ │ │ ├── index.8372475c.less │ │ │ ├── index.85c775e4.less │ │ │ ├── index.8c12967b.less │ │ │ ├── index.976fe83e.less │ │ │ ├── index.cae8fdaf.less │ │ │ ├── index.d15ddbc9.less │ │ │ ├── index.d61ddb9a.less │ │ │ ├── index.e1e14bcc.less │ │ │ ├── index.e90871b5.less │ │ │ └── index.module.b57695f6.less │ │ └── template.md │ ├── tmp_end_verified/ │ │ └── .gitkeep │ ├── tool/ │ │ ├── download_data/ │ │ │ └── .gitkeep │ │ ├── folder/ │ │ │ └── .gitkeep │ │ ├── tar/ │ │ │ └── .gitkeep │ │ ├── upload_data/ │ │ │ └── .gitkeep │ │ └── verify_tar/ │ │ └── .gitkeep │ └── verified/ │ └── .gitkeep ├── salt/ │ ├── master │ ├── minion │ ├── minion.d/ │ │ └── _schedule.conf │ └── minion.template └── scripts/ ├── cmd_manager ├── install.sh ├── omp ├── source/ │ ├── __init__.py │ ├── add_readonly_user.py │ ├── cmd_install_entrance.py │ ├── cron │ ├── features.py │ ├── install_mysql_redis.py │ ├── install_or_update.py │ ├── omp_rollback.py │ ├── omp_salt_agent │ ├── omp_upgrade.py │ ├── repair_dirty_data.py │ ├── salt │ ├── salt_agent_manager │ ├── scan_tar_file.py │ ├── scan_tools.py │ ├── service_manager.py │ ├── tengine │ ├── uninstall_app_store.py │ ├── uninstall_services.py │ ├── update_conf.py │ ├── update_data.py │ ├── update_grafana.py │ ├── update_monitor_agent.py │ ├── upgrade_service.py │ ├── uwsgi │ └── worker └── uninstall.sh ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /omp_web/node_modules ### Python template # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # PEP 582; used by e.g. github.com/David-OConnor/pyflow __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ ### Example user template template ### Example user template # IntelliJ project files .idea *.iml out gen # General .DS_Store omp_web/.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon�� # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk /jon_test_files/ /package_hub/127.0.0.1/ temp /package_hub/omp_monitor_agent.tar.gz ================================================ FILE: .pre-commit-config.yaml ================================================ default_stages: [commit] repos: - repo: https://github.com/yingzi113/pre-commit-hooks rev: 5863e162f1bed1f63eeb716e77d622ff8e3d9af9 hooks: - id: check-case-conflict - repo: https://github.com/pre-commit/mirrors-autopep8 rev: v1.4.4 hooks: - id: autopep8 args: [-i, --global-config=.flake8, -v, --ignore=E402] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.4.0 hooks: - id: flake8 args: - --ignore=E501,E402 exclude: package_hub/_modules/init_host.py - id: check-docstring-first - id: trailing-whitespace - id: check-ast - id: check-json - id: check-yaml exclude: migrations ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ # Readme # OMP-运维管理平台 OMP(Operation Management Platform)是云智慧公司自主设计、研发的轻量级、聚合型、智能运维管理平台。是一款为用户提供便捷运维能力和业务管理的综合平台。具备运维一应俱全的功能,目的是提升运维自动化、智能化,提高运维效率,提升业务连续性和安全性。 # OMP设计初衷 ## 目前运维面临的痛点: - 主机环境多样性,难以统一管理:如:混合云、私有云、跨IDC、虚拟化、容器化等 - 业务变更难度较大,自动编排能力较低 - 业务状态监控,多平台难以数据联动 - 业务出现异常,难以实现故障自愈 - 业务运行状态,难以评估,更难以分析 - 运维知识匮乏,缺少专家指导及专家解决方案 运维管理平台(OMP)的设计初衷就是想打造一应俱全的运维平台,降低交付难度,提升运维自动化、智能化,提高运维效率,提升业务连续性和安全性。 # OMP核心功能 - **主机纳管**:纳管主机资源,实时监控主机运行状态,可在线管理、在线连接终端等 - **应用管理**:平台已内置常用基础组件,也支持符合标准的自研服务发布到应用商店,从而提供便捷的应用管理,如:安装部署、变更发布、弹性扩缩容、在线配置、优化等 - **应用监控**:涵盖标准监控、定制监控、链路监控、智能监控等多种业务场景,通过大数据智能测算,可感知未来趋势,将异常控制在发生前 - **故障自愈**:当业务系统出现异常或故障时,可按照预定的自愈策略进行故障治理,极大降低故障对业务影响,减少企业损失 - **状态巡检**:自动、手动进行业务指标、运行状态汇总,支持自动发送报告到指定邮箱 - **备份/恢复**:针对核心数据进行本地+异地备份,支持自动执行备份并将数据发送至指定邮箱,达到异地的存储效果,确保数据安全 - **精简工具**:提供运维常用工具、命令、脚本、SQL等,为日常运维操作提供便利,减少误操作、减低技术门槛,支持自行维护、扩充更多工具 - **权限管理**:针对不同用户、角色,进行权限控制,及操作审计 # 架构设计 ![./doc/architecture.png](./doc/architecture.png) ## Demo 通过浏览器访问页面,访问入口为:http://omp.cloudwise.com/#/login \ 默认用户名:admin \ 默认密码:Yunweiguanli@OMP \ 每晚 00:00 将重置数据 # 使用 OMP ## 安装部署 当前OMP安装包内部包含了其使用的所有组件,建议将OMP部署在 /data/ 下,当前版本部署流程如下: \ step0:下载解压安装包 ```shell tar -xvf omp_open-*.tar.gz -C /data ``` step1:编辑文件,检查环境配置 ```shell vim /data/omp/config/omp.yaml ``` 注意:当前版本已携带mysql、redis安装,配置信息如下,如需修改请在安装前修改 ```yaml # redis相关配置 redis: host: 127.0.0.1 port: 6380 password: common123 # mysql相关配置 mysql: host: 127.0.0.1 port: 3307 username: common password: Common@123 ``` step2:执行安装脚本 ```shell cd /data/omp && bash scripts/install.sh # 注意1:执行后根据提示选择本机ip,如主机上存在多网卡多IP情况,需要根据业务需求自行判断使用哪个ip地址 # 注意2:当前执行操作的用户即为OMP中各个服务进程的运行用户,在以后的维护中,也应使用此用户进行操作 ``` ## 管理OMP 注意:如需停止 OMP 相关服务,请先执行 “停止 OMP 定时保活任务” 操作 ```shell # [服务名称] 值为: all 为对所有组件操作 # all|mysql|redis|tengine|uwsgi|worker|cron|salt|prometheus|alertmanager|grafana|loki bash /data/omp/scripts/omp [服务名称] [status|start|stop|restart] ``` 停止 OMP 定时保活任务: ```Apache # 查看定时任务 crontab -e # 删除或注释如下内容,否则定时任务会将 OMP 自动拉起 # */5 * * * * bash /data/omp/scripts/omp all start &>/dev/null ``` ## 卸载OMP omp节点上卸载操作如下: ```shell bash /data/omp/scripts/uninstall.sh ``` ## 升级 & 回滚 OMP ```shell # 升级命令 bash cmd_manager omp_upgrade [必填参数:升级目标路径(如:/data/omp,注意此处路径末尾无/)] [选填参数:从某个断点处升级,默认开头] # 例如 bash 升级包路径/scripts/cmd_manager omp_upgrade /data/omp(当前正在运行的旧安装路径) # 回滚命令 bash cmd_manager omp_upgrade [必填参数:升级目标路径(如:/data/omp,注意此处路径末尾无/)] [选填参数:从某个断点处升级,默认开头] # 例如 bash 升级包路径/scripts/cmd_manager omp_rollback /data/omp(当前正在运行的旧安装路径) ``` ## 断点执行 常用于执行过程中某一步骤失败时,期望从失败步骤处再次执行时使用,正常情况无需考虑此参数,参数默认下标为0 升级回滚可以理解成为jenkins的pipliene 是分步骤执行的,当我们在某一个位置出现异常时,手动修复后通过错误节点再次进行时使用,而跳过之前已经升级(回滚)正确的步骤 ```shell # 升级流程顺序如下: # PreUpdate, Mysql, Redis, Grafana, Tengine, OmpWeb, OmpServer, Python, PostUpdate ``` # 环境依赖 ## 技术栈 ### 后端技术栈 - Python 3.8.7 - Django 3.1.4 - Saltstack 3002.2 - Uwsgi 2.0.19.1 ### 数据库 - mysql 5.7.37 - redis 6.2.7 ### 前端技术栈 - Tengine 1.22.0 - React 17.0.1 ### 监控技术栈 - Prometheus 2.25.1 - Alertmanager 0.24.0 - Grafana 9.3.8 - Loki 2.4.1 - Promtail 2.2.0 ## 内置组件概览 | **组件名称** | **组件作用** | **端口** | | ------------ | ------------------------------------------------ | ------------ | | tengine | 平台访问入口,代理前端页面及后端uwsgi程序 | 19001 | | uwsgi | web容器,用于提供 python Django 后端程序访问入口 | 19003 | | salt | 开源组件,服务器控制程序,提供主机 Agent 通信 | 19004、19005 | | worker | 异步任务、定时任务执行程序,有进程无端口 | - | | prometheus | 开源组件,提供监控数据 | 19011 | | grafana | 开源组件,提供监控面板 | 19014 | | alertmanager | 开源组件,提供日志告警 | 19013、9094 | | loki | 开源组件,提供日志采集 | 19012、9095 | | redis | 开源组件,提供缓存,消息队列 | 6380 | | mysql | 开源组件,数据存储 | 3307 | | ntpd | 开源组件,提供时间同步功能 | 123(udp) | # 关于应用商店 ## 如何制作一个OMP应用商店中的应用 [OMP 社区版-应用商店发布说明文档](./doc/app_publish.md) > 内含 - 基础组件打包规范 - 应用服务打包规范 - 目录和配置说明 - postgreSql、redis、rocketmq等应用Demo ## 卸载应用商店中已经发布的应用 >已支持界面操作 ```shell export LD_LIBRARY_PATH=/data/omp/component/env/lib && /data/omp/component/env/bin/python3.8 /data/omp/scripts/source/uninstall_app_store.py --product 产品名称 --app_name 组件/服务名称 --version 版本 ``` 已经部署服务实例的安装包,无法卸载 参数说明: 1. ***--version*** 缺省时,卸载所有版本 2. 卸载基础组件 ***--app_name 基础组件名称*** 3. 卸载应用/产品 ***--product 应用/产品名称*** 4. 卸载应用下指定服务 ***--product 应用/产品名称 --app_name 服务名称*** 欢迎加入 获取更多关于OMP的技术资料,或加入OMP开发者交流群,可扫描下方二维码咨询 ================================================ FILE: UpdateLog.md ================================================ # 更新日志 ------ ## v0.1.0 (2021.11.30) - 【仪表盘】 - 全局状态概览 - 当前异常信息展示 - 各模块状态展示 - 【主机管理】 - 主机纳管(添加、导入、编辑、维护、删除) - 主机自动监控、告警 - 【应用商店】 - 组件、应用WEB发布、服务端自动发现 - 组件、应用部署,自动编排解决依赖 - 【服务管理】 - 服务管理(启动、停止、重启、删除) - 服务监控(监控、日志、告警、自愈) - 【应用监控】 - 实时展示处于异常的主机、服务信息,呼应仪表盘的异常清单 - 告警历史记录查看,未读提醒,按添加检索 - 支持监控组件地址自定义,便于对接现有监控平台 - 【状态巡检】 - 支持主机巡检、组件巡检、深度分析,且支持导出 - 支持定时自动执行巡检任务 - 【系统管理】 - 用户账户管理 - 支持全局维护模式,避免人为操作时误报 ## v0.5.0 (2022.04.11) - 【应用商店】 - 组件、应用服务的升级及回滚 - 应用服务的增量安装 - 【部署模板】 - 支持通过部署模版实现批量部署 - 【应用监控】 - 支持告警邮件配置,将告警信息发送至指定邮箱 - 【故障自愈】 - 展示故障自愈记录 - 支持监控到服务状态异常后自动进行重启 - 支持设置服务自愈尝试次数 - 【指标中心】 - 支持添加自定义告警指标规则 - 添加自定义扩展采集指标 - 【数据备份】 - 支持mysql、arangodb、postgreSql数据备份 - 备份记录展示、下载、删除 - 支持自定义保存路径、定时备份策略及邮件推送备份内容 - 【实用工具】 - 内置部分运维实用小工具 - 展示小工具执行过程、输出展示及生成文件下载 - 【系统管理】 - 增加邮件管理,支持设置smtp邮件服务器作为全局邮件发件箱 - 【平台优化】 - 优化主机纳管逻辑,增加纳管成功率,支持删除主机 - 优化应用安装服务逻辑代码 - 优化巡检逻辑 - 优化部分前端页面显示及交互效果 - 【其他】 - 修复已知bug ## v0.6.0 (2022.11.29) - 升级内置基础组件和环境 - alertmanager 升级至 v0.24.0 - tengine 升级至 v1.22.0 - 扩充内置环境中部分第三方库 - 升级主机 Agent & 监控 Agent - 优化小工具异步任务执行逻辑 - 更新 prometheus 和 loki 的配置 - 修复 grafana 面板中 mysql 显示异常问题 - 补充应用商店基础组件包:mysq、elasticsearch - 组件包从代码库抽离,减少源码 & 包体量 ## v0.7.0 (2022.12.30) - 完善 OMP 管理脚本功能 - 支持升级、回滚,支持断点重试 - 支持命令行卸载应用商店已发布服务 - 内置 Redis 5.0.37 升级至 6.2.7 - 验证码登陆 - 修改密码长度异常问题 ## v0.8.0 (2023.01.30) - 新增监控功能 - 产品http请求 5XX 错误 - jvm 文件句柄使用率过高 - 修复部分服务无法获取 cpu、内存问题 - 增加只读用户功能 - 修复添加主机提示已经存在问题 - 银河麒麟V10 ARM ,鲲鹏920 (ARM架构)2023.03.30 ## v0.9.0 (2023.05.31) - 内置Grafana版本升级至 9.3.8 - 主机/服务详情页面布局调整 - 主机/服务/安装/升级/回滚页面中文本溢出处理 - 支持通过前端界面方式卸载应用商店中已经发布的应用 - 更新readme文档 ## v1.0.0 (2023.07.30) - 新增功能【服务纳管】模块 - 重构【服务自愈】模块 - 新增 OOM 告警 - 更新部分 Grafana 面板 - 服务面板: redis、victoriametrics、rocketmq - 集群面板: redis、clickhouse、mysql、tengine - 修复 Grafana 无法登陆问题 ## v1.1.0 (2023.09.30) - 重构「数据备份」模块 - 支持多端口服务监控 & 更新文档 - 前端优化,步骤类交互型界面,刷新自动跳转 - 前端优化,消除部分冗余导入 - 修复bug:仪表盘异常清单类型缺失,环形统计图跳转增加类型过滤,nodeExporter、loki启动失败问题 ================================================ FILE: component/.gitkeep ================================================ ================================================ FILE: component/alertmanager/.gitkeep ================================================ ================================================ FILE: component/grafana/.gitkeep ================================================ ================================================ FILE: component/loki/.gitkeep ================================================ ================================================ FILE: component/prometheus/.gitkeep ================================================ ================================================ FILE: config/omp.yaml ================================================ # 全局用户, 自动解析当前操作用户 global_user: common # 初始化时由用户输入本机的ip地址 local_ip: 10.0.1.160 # SSH执行命令超时时间,单位秒 ssh_cmd_timeout: 60 # SSH连通性校验超时时间,单位秒 ssh_check_timeout: 10 # 线程池最大workers thread_pool_max_workers: 10 # redis相关配置 redis: host: 127.0.0.1 port: 19034 password: common123 # mysql相关配置 mysql: host: 127.0.0.1 port: 19033 username: common password: Common@123 # salt相关配置 salt_master: publish_port: 19004 ret_port: 19005 timeout: 30 # uwsgi的配置 uwsgi: socket: 127.0.0.1:19003 processes: 4 threads: 2 # tengine相关的配置 tengine: access_port: 19001 runserver_port: 19002 # 登录token过期时间,天 token_expiration: 1 #grafana认证字段,无需修改 grafana_api_key: test #grafana_auth grafana_auth: # grafana admin auth grafana_admin_auth: username: admin plaintext_password: Yunweiguanli@OMP_123 # grafana viewer auth grafana_viewer_auth: username: omp plaintext_password: Common@123 # 关联邮件设置,谨慎修改 alert_manager: # 是否开启发送邮件配置,默认不开启 send_email: false # 发件人邮箱配置 EMAIL_SEND: <发件人邮箱> # smtp服务器地址端口配置 SMTP_SMARTHOST: # 解释待定???? SMTP_HELLO: 163.com # 发件人的用户名配置 EMAIL_SEND_USER: <发件人的用户名> # 发件人邮箱秘钥 EMAIL_SEND_PASSWORD: <发件人邮箱密钥> # 发送频率(同一条报警消息的发送频率,s、m、h对应 秒、分、小时) EMAIL_SEND_INTERVAL: 30m # 接收代号 RECEIVER: commonuser # 收件人 EMAIL_ADDRESS: <收件人邮箱> # webhook地址,仅在手动维护OMP各个组件时才可能用到,其他情况下不建议修改 WEBHOOK_URL: http://127.0.0.1:19001/api/promemonitor/receiveAlert/ # prometheus basic auth prometheus_auth: username: omp plaintext_password: Yunweiguanli@OMP_123 ciphertext_password: $2b$12$R8WlKOEV2M9iBjEhnWQORepigbQoD1D/rAyEXwIu/aS5t94deTVDu # 监控使用相关端口配置 monitor_port: # server各个端口 prometheus: 19011 loki: 19012 alertmanager: 19013 grafana: 19014 # agent端各个端口配置 blackboxExporter: 19015 promtail: 19016 nodeExporter: 19017 processExporter: 19018 mysqlExporter: 19019 redisExporter: 19020 kafkaExporter: 19021 zookeeperExporter: 19022 clickhouseExporter: 19023 postgreSqlExporter: 19024 beanstalkdExporter: 19025 tengineExporter: 19026 elasticsearchExporter: 19027 httpdExporter: 19028 igniteExporter: 19029 rocketmqExporter: 19032 # arangodb与nacos没有单独的exporter,使用的是arangodb和nacos的原生自带接口 arangodbExporter: 18119 nacosExporter: 18117 monitorAgent: 19031 # Loki相关配置 loki_config: # promtail采集日志文件名等级过滤 [debug, info, warn, error, all] scrape_log_level: error # 基础及公共组件间的等级划分,不要将自研服务类的服务放入下面的配置 # 用于控制服务安装过程中的执行顺序、服务的启停控制顺序 # 安装时顺序从小到大执行,同级别并发执行 basic_order: 0: - jdk - safeRM - tengine - mysql - comLib - redis - arangodb - minio - postgreSql - prometheus - pushgateway - mongodb - nodejs - beanstalk 1: - keepalived - httpd - elasticsearch - zookeeper - rocketmq - wkhtmltox - tomcat - jkbPhp 2: - nacos - xxlJob - kafka - clickhouse - hadoop 3: - flink - aopsUtils - filebeat - victoriaMetrics - sentinel # 自愈周期内服务key的存活时常 min health_redis_timeout: 60 # 单次health接口检测最大次数 health_request_count: 10 # 单次health接口请求停留时常 s health_request_sleep: 6 # 模板安装下,基础组件集群模式个数的严格校验 template_cluster_check: true #服务发现脚本,纳管自定义排查脚本 service_discovery: backup_service: - mysql - postgreSql ================================================ FILE: config/private_key.pem ================================================ -----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAwv4dqlvcYtrPJsCL/VuX0u4FZm2E0du1m01gUnp3afSkx+u2 GTXptpS7dNfTLguu1HjJUzkEIGaJGG/x/PR2Zs6I/UmIFWj6tdmfBBlrVRETnm8t CAdO9/1zjzz4wB2yuHduBK6TYwhXfZOCg3LOj+QVpUYqyq3lqjPN+C6QbFzgk8Fw MHr+R3OzZe9nsaNZOHRbSmu6NU5zkIdnScQwIiWIe9nZMpoTUe45FtYPj7SHiCIt DOtbXGbyNOP5k5RhIQtJiEJgVOzGeRSaQAj6UfLClsa43ZXfXIZ+BfOO8GZBXmHe RHRs/Prw3Io4n0gXKpgrd6MxwfpoxmXEyoRUGwIDAQABAoIBAQCBjcL6CFSSHZ0a uz2HlQ53p4tQ9Z0Urayoxa0kv5eNf2zoI5T2hRqGI6W0yRzXcA21v5bLw4sZV+bo pKAcF/R+8+SSnQNcbkZ9Al0jpRvqBhGJ54X82pY+MFhSKAmB43l2FGu1kqP8XXN7 zMEfQu05Lyquh8MwrH92KTtFFPMB+z5BQqukXbZxhkVjEEuDtfvHyw5hQkv4PtM8 Z+pd9Wh7/WMOq7dJn6nM+TVWQjz+nqsulCEHZ1oj2m4xk57b4QP9wm0DNe0k/bo5 6b7U/2jvtLeqhFbN2hp1fzHpqo4hDZTuYv9lBFlLGMnKPoppi+oYW8APmy123DCn mWmKn3oJAoGBAOqKMRzLCGytbW+qeFhJgohhNnuYxRV5fzG5EUbOb8zO2nZO5QUw hJApWDW/mOIvezr53UgLvadw9pnJdDQoQX8gxBX6j6eWbJvXwDU+pBw9Nl3U0ZTZ Mu8TDy1k/Up59B5BwUInDX2klPJQI/vjf9KoSKXinBbXRkKURq+fMi1lAoGBANTV lHWegOdKTzh9PDGbNcQ7KpO89WmLSJQXR54UsOs4x1Kcn0kOWTKV6piPJbAsGzXR ucs9FfcQukDL/JabEeH0k3NedXCwR43SMTby7f1Sl+8nWdyu4Rf5Z4/iZq+JrbYu bJ10ijbnpVCBQxrZ1ce88fyF56mbG0wry4IljyN/AoGBANQed5ya49umXjuH6Z+v nCbMBQJzgIuTfr3xqvZm7iZFTr+BSxAOeVYIjobN6e9nEgScxszKEZTGTcF4uWgS oGnhsHZQTmw7V676yhNdu/7uPaVPPN1qMu6WRjvAAnTBJ0/WGHtD5qejmjIs2N6P OqPDHzEoahMeT6UXhXaAfFkhAoGADB3YpNWQOxqk5e9jROO0LOa9ZsnEIu0WBbBJ mHtPEyUZW9+kxdD2TQXx5BuKJrxsFCVLcYGZxYYDRHsYdy5+1yFIX7IJ949hk3Za 7OjpmZlhIvFXkVO3ZtkBB1T5SZcJ96wu7MvcroGDjNC/FEFAhW2BTUIGTUaSSETa Ah/HRVsCgYEAuoJK9FDdCDsZmAfXSo4T3JtGx1yvKQ1tFCREC2CEQ98S2aEc0tRZ CvUUresVdN1AuxtYVDxUOtZPD5GUCrgR+z4heZNi25j6mCf51NAgnaV170sAOOn6 FjRXOYf0UFAo1eGZjsBp4bQdmdXxynzuW2jbNRxw8mZlHCBBx5jb1O8= -----END RSA PRIVATE KEY----- ================================================ FILE: config/product.yaml ================================================ # 使用配置文件的方式更新产品的yaml,留出可更改接口 # 更新安装参数,如下安装参数在安装过程中会进行更改替换 install: nacos: - name: "租户类型1单2多3saas" key: "deploy_type" default: "1" editable: true # 更新端口配置,如下端口在安装过程中会更新到端口中 ports: testService: - name: 服务端口 protocol: TCP key: service_port default: 18125 ================================================ FILE: config/salt/master ================================================ interface: 0.0.0.0 publish_port: 19004 ret_port: 19005 user: root enable_ssh_minions: False presence_events: True auto_accept: True timeout: 30 root_dir: /data/omp/data/salt conf_file: /data/omp/config/salt/master file_roots: base: - /data/omp/package_hub file_recv: True file_recv_max_size: 524288 ================================================ FILE: config/salt/minion ================================================ master: ${MASTER_IP} master_port: ${MASTER_PORT} user: ${USER} id: ${AGENT_ID} root_dir: ${AGENT_DIR}/data/salt conf_file: ${AGENT_DIR}/config/salt/minion ================================================ FILE: config/salt/minion.d/_schedule.conf ================================================ schedule: __mine_interval: {enabled: true, function: mine.update, jid_include: true, maxrunning: 2, minutes: 60, return_job: false, run_on_start: true} ================================================ FILE: config/salt/minion.template ================================================ master: ${MASTER_IP} master_port: ${MASTER_PORT} user: ${USER} id: ${AGENT_ID} root_dir: ${AGENT_DIR}/data/salt conf_file: ${AGENT_DIR}/config/salt/minion ================================================ FILE: data/.gitkeep ================================================ ================================================ FILE: data/inspection_file/.gitkeep ================================================ ================================================ FILE: doc/app_publish.md ================================================ # OMP 社区版-应用商店发布说明文档 [TOC] ## 1. 说明 用户可以在应用商店发布“基础组件”与“应用服务”两个维度的产品,在区分上,应用服务可以理解为完整的提供某一类服务的产品,产品内部可由一个或多个“服务”组成 ,比如gitlab、jenkins等。基础组件的角色更多是作为其他完成产品的一部分存在,以完成产品的某些功能需求,如mysql、redis等。 ## 2. 基础组件打包规范 注:用户在发布基础组件安装包时,需按照以下规范打包上传才可以正常发布 ### 2.1. 目录规范 以MySQL服务为例,需将涉及到的文件统一放在 mysql目录下,目录名称与该服务名称保持一致,目录中需要提供与该目录名称一致的配置文件(如:mysql.yaml)、产品图标(如:mysql.svg) 和其他所需文件(如安装脚本等) **示例:** ```shell $ tree ./mysql -L 2 ./mysql # 目录名称,请与组件名称一致 ├── mysql.svg # 平台展示组件图标,请使用 “组件名称.svg ” 命名,与目录名称保持一致 ├── mysql.yaml # 组件配置文件, 记录该组件安装、升级等所需信息, 请使用 “组件名称.yaml” 命名,与目录名称保持一致 └── scripts # 组件的安装、启动等控制脚本,该脚本在安装时会调用 │   ├── init.py # 初始化脚本 │   ├── install.py # 组件安装脚本 │   ├── mysql # 组件启动、停止控制脚本,建议与服务名称一致 │   ├── mysql_backup.py # 其他动作脚本,如备份等 ``` **备注:** 1. 组件图标请使用svg格式图片,如不添加会显示平台缺省图标; 2. 确保目录名称(mysql)、配置文件(mysql.yaml) 、图标(mysql.svg) 名称统一, 上传安装包时,平台将根据名称校验对应文件合法性,如名称不一致,可能会导致校验不通过等问题; 3. 确保安装包解压后是一个整体目录 ### 2.2. 压缩包命名规范 请使用 `{name}-{version}-{others}-{package_md5}.tar.gz` 格式进行打包命名 1. name: 安装包名称,建议字符: `英文` `数字` `_` 2. version: 安装包版本,建议字符: `英文` `数字` `_` `.` 3. others: 其他信息,建议字符: `英文` `数字` `_` `.` 4. package_md5: 安装包MD5 值 例如:`mysql-5.7.31-beta-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz` ```shell $ tar czf mysql-5.7.31.tar.gz mysql $ md5sum mysql-5.7.31.tar.gz 8e955b24fefe7061eb79cfc61a9a02a1 $ mv mysql-5.7.31.tar.gz mysql-5.7.31-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz ``` ### 2.3. 配置文件(yaml)说明 平台预留KEY值(该KEY值存在指定定义,请准确使用): | KEY | 说明 | 备注 | | ------------ | ------------ | -------------------------------------- | | service_port | 服务端口 | 供其他程序连接的端口号 | | base_dir | 应用安装目录 | | | log_dir | 应用日志目录 | 服务的日志采集会采集该目录下*.log 文件 | | data_dir | 应用数据目录 | | | username | 用户名 | | | password | 密码 | | ```yaml # 类型定义,发布基础组件时 ,指定类型为 component (类型:string) kind: component # 组件在平台显示的名称,请与组件目录名称保持一致,建议字符:英文、数字、_ (类型:string) name: mysql # 上传后显示的组件版本,建议字符: 数字、字母、_ 、. (类型:string) version: 5.7.31 # 组件描述信息,建议长度256字符之内,请针对组件书写贴切的描述文字 (类型:string) description: "MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。" # 组件所属标签,请针对组件功能设置准确标签,平台会针对该标签对组件进行分类,(类型:list[string,string...]) labels: - 数据库 # 指定该服务安装后是否需要启动 (类型:boolean) auto_launch: false # 指定组件是否为基础环境组件,如 jdk, 该类组件以基础环境方式安装 (类型:boolean) base_env: flase # 定义组件所需端口号,如不启用端口,可留空 (类型:list[map,map...]) ports: # 端口描述名称,用户在安装时会以该名称显示表单内容(类型:string) - name: 服务端口 # 端口协议,支持 TCP/UDP protocol: TCP # 端口英文描述名称,该key会传入到安装脚本中 (类型: string)支持(英文、数字、_) key: service_port # 注:service_port 为保留关键词,表示 为 提供服务的端口 # 组件的默认端口号,在安装时,会以该值填入表单中(类型: int) default: 3306 # 组件监控相关配置,定义该组件在安装后如何监控 ,如果不需要监控可留空 (类型: map) monitor: # 监控进程名称,如“mysqld”,平台在发现mysqld进程不存在后,会发送告警提醒 ,不需要监控可留空(类型:string) process_name: "mysqld" # 监控端口号,请根据 ports 中的变量设置,不需要监控可留空 (类型: {string}) metric_port: {service_port} --- # 设置集群模式方式,如果组件需要支持多种方式安装,可以在该字段中定义,如只支持单个实例安装,可留空(类型:map[list[map,map...]]) deploy: # 定义单实例模式安装 (类型:list[map,map...]) single: # 部署方式的中文描述名称,该值会在表单中选择集群模式时显示 (类型:string) - name: 单实例 # 该模式的key值 (类型:string) key: single # 定义多种集群模式安装 (类型:list[map,map...]) complex: # 部署方式的中文描述名称,该值会在表单中选择集群模式时显示 (类型:string) - name: 主从模式 # 该模式的key值 (类型:string) key: master_slave # 集群节点设置 (类型: map) nodes: # 初始节点数量 (类型:int) start: 2 # 增加节点步长 (类型:int) step: 1 # 定义该组件安装所需依赖组件名称与版本,如不需其他组件依赖,可留空 (类型: list[map,map..]) #例: #dependencies: # - name: jdk # version: 8u223 dependencies: # 该组件所需最小资源需求 (类型:map) resources: # cpu最小需求 ,1000m 表示 1核 (类型:string) cpu: 1000m # 内存最小需求, 500m 表示 500兆内存 (类型:string) memory: 500m --- # 定义安装组件时所需参数,该参数会传入到 安装脚本中 (类型:list[map,map...]) install: # 传入参数中文描述名称,该名称会在用户安装组件时显示到表单中 (类型: string) - name: "安装目录" # 传入参数key值,会将该key与值 传入到安装脚本中 (类型:string) key: base_dir # 上面key默认值 (类型: stirng) default: "{data_path}/mysql" # 注: {data_path} 为主机数据目录占位符,请勿使用其他代替 - name: "数据目录" key: data_dir default: "{data_path}/mysql/data" - name: "日志目录" key: log_dir default: "{data_path}/mysql/log" - name: "用户名" key: username default: root - name: "密码" key: password default: "123456" # 程序控制脚本与服务目录的相对路径 (类型:map) control: # 启动脚本路径,如没有可留空 (类型:string) start: "./scripts/mysql start" # 停止脚本路径,如没有可留空 (类型:stirng) stop: "./scripts/mysql stop" # 重启脚本路径,如没有可留空 (类型:stirng) restart: "./scripts/mysql restart" # 重载脚本路径,如没有可留空 (类型:stirng) reload: # 安装脚本路径,必填 (类型:stirng) install: "./scripts/install.py" # 初始化脚本路径,必填 (类型:stirng) init: "./scripts/init.py" ``` ### 2.4. 安装脚本编写说明 在安装包成功发布后,可通过平台进行安装,平台会调用配置文件中指定的安装脚本进行程序安装,平台将会把安装脚本所需参数以如下形式进行传参,需要脚本在编写时对此进行支持。 传参示例: ```shell $ python ./scripts/install.py --local_ip 192.168.1.2 --data_json /data/LKJD82JDL.json ``` 其中 local_ip 为安装主机的IP地址,data_json为安装所需数据文件路径 安装脚本需要根据data_json内数据进行组件的安装、替换其他文件内的占位符 data.json示例: ```json [ { "name":"nacos", "ip":"1.1.1.1", "version":"2.0.1", "ports":[ { "key":"service_port", "name":"xxx端口", "default":8001 } ], "install_arg":[ { "key":"base_dir", "name":"服务目录", "default":"/data/app/nacos" }, { "key":"data_dir", "name":"数据目录", "default":"/data/appData/nacos" }, { "key":"username", "name":"用户名", "default":"admin" }, { "key":"password", "name":"密码", "default":"admin123" } ], "deploy_mode":{ }, "cluster_name":"", "instance_name":"nacos-1", "dependence":[ { "name":"mysql", "instance_name":"mysql-100", "cluster_name":"mysql-JDLK3KA" } ] }, { "name":"mysql", "ip":"192.1.2.3", "version":"5.0.1", "ports":[ { "key":"service_port", "name":"服务端口", "default":10601 } ], "install_arg":[ { "key":"base_dir", "name":"服务目录", "default":"/data/app/mysql" }, { "key":"data_dir", "name":"数据目录", "default":"/data/appData/mysql" }, { "key":"data_dir", "name":"日志目录", "default":"/data/appData/log" }, { "key":"username", "name":"用户名", "default":"root" }, { "key":"password", "name":"密码", "default":"root123" } ], "deploy_mode":{ }, "cluster_name":"", "instance_name":"mysql-100", "dependence":[ ] } ] ``` ### 2.5 打包好的应用Demo 查看目录: omp/package_hub/back_end_verified ```shell filebeat-7.12.0-20220311171107-3e85bed.tar.gz httpd-2.4.46-20220120145141-b1a6fa6.tar.gz keepalived-2.1.5-20220120145217-8c6aaf7.tar.gz postgreSql-13.5.0-20220411212618-6fabab8.tar.gz redis-5.0.14-20220411212556-6eb6e28.tar.gz rocketmq-4.8.0-20220410214539-74feff6.tar.gz ``` ## 3. 应用服务打包规范 ### 3.1. 目录规范 在发布类别为应用服务的产品时,需要将产品名称、所属产品的服务名称、版本号做到全局统一 **目录示例:** 发布产品名称为“omp",其中包含 3个服务为“omp_server","omp_web","omp_component" 的目录结构如下 ```shell $ tree omp omp ├── omp.svg # 定义产品图标,会在平台中展示,如果不创建则平台会展示缺省图标 ├── omp # 定义产品下服务配置文件目录,将所需服务的配置文件存在该目录 │ ├── omp_server.yaml # 服务 omp_server 配置文件,文件名需要与服务名称一致 │ ├── omp_web.yaml # 服务 omp_web 配置文件,文件名需要与服务名称一致 │ └── omp_component.yaml # 服务 omp_agent 配置文件,文件名需要与服务名称一致 ├── omp_server-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_server 压缩包,以“-” 为分隔符,第一个为服务名称,需要与服务名称一致,格式为 {service_name}-{service_version}-{others}-{package_md5}.tar.gz ├── omp_web-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_web 压缩包 ├── omp_component-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_agent 压缩包 └── omp.yaml # 定义产品配置文件,文件名需要与产品名称一致 ``` 其中服务目录以omp_server为例: ```shell $ tree omp_server omp_server # 服务包解压后目录名称,与服务名一致 ├── bin # 服务控制脚本目录,启动、停止等 │ └── omp_server # 服务控制脚本,与服务名称一致 ├── omp_server.yaml # 服务配置文件,与产品包中保持一致 └── scripts # 安装、升级脚本目录 ├── init.py # 初始化脚本 ├── install.py # 安装脚本 └── update.py # 升级脚本 ``` ### 3.2. 压缩包命名规范 请使用 `{name}-{version}-{others}-{package_md5}.tar.gz` 格式进行打包命名 1. name: 安装包名称,建议字符: `英文` `数字` `_` 2. version: 安装包版本,建议字符: `英文` `数字` `_` `.` 3. others: 其他信息,建议字符: `英文` `数字` `_` `.` 4. package_md5: 安装包MD5 值 例如: omp-0.1.0-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz ### 3.3. 配置文件yaml说明 发布类别为应用服务的产品时,需分别对 产品配置文件和产品下服务配置文件进行配置 #### 3.3.1. 产品配置文件(yaml)格式说明 ```yaml # 类型定义,发布应用服务时,产品指定类型为 product (类型:string) kind: product # 定义产品名称,此名称需要与产品目录名称、产品配置文件名称保持一致,建议字符:英文、数字、_ (类型: string) name: omp # 上传后显示的产品版本,建议字符: 数字、字母、_ 、. (类型:string) version: # 产品描述信息,建议长度256字符之内,请针对产品书写贴切的描述文字 (类型:string) description: "运维管理平台(OperationManagementPlatform,以下简称OMP)以管理服务为中心,为服务的安装、管理提供便捷可靠的方式。" # 组件所属标签,请针对组件功能设置准确标签,平台会针对该标签对组件进行分类,(类型:list[string,string...]) labels: - omp # 定义该产品安装所需依赖产品名称与版本,如不需其他产品依赖,可留空 (类型: list[map,map..]) dependencies: # 定义该产品下包含的服务信息,请确保列表中的服务包都包含在目录中,并且名称保持一致 (类型: list[map,map...]) service: # 包含服务名称,请与服务包名保持一致 (类型: string) - name: omp_server # 服务版本,请与服务包版本一致 (类型:string) version: 0.1.0 - name: omp_web version: 0.1.0 - name: omp_component version: 0.1.0 ``` #### 3.3.2. 服务配置文件(yaml)格式说明 ```yaml # 类型定义,发布应用服务时,产品包含的服务指定类型为 service (类型:string) kind: service # 服务在平台显示的名称,请与服务目录名称保持一致,建议字符:英文、数字、_ (类型:string) name: omp_server # 上传后显示的服务版本,建议字符: 数字、字母、_ 、. (类型:string) version: 0.1.0 # 服务描述信息,建议长度256字符之内,请针对组件书写贴切的描述文字 (类型:string) description: "服务描述内容..." # 指定该服务安装后是否需要启动 (类型:boolean) auto_launch: true # 指定服务是否为基础环境组件,如 jdk, 该类组件以基础环境方式安装 (类型:boolean) base_env: flase # 定义服务所需端口号,如不启用端口,可留空 (类型:list[map,map...]) ports: # 端口描述名称,用户在安装时会以该名称显示表单内容(类型:string) - name: 服务端口 # 端口协议,支持 TCP/UDP protocol: TCP # 端口英文描述名称,该key会传入到安装脚本中 (类型: string)支持(英文、数字、_) key: service_port # 注:service_port 为保留关键词,表示 为 提供服务的端口 # 组件的默认端口号,在安装时,会以该值填入表单中(类型: int) default: 19001 # 服务监控相关配置,定义该服务在安装后如何监控 ,如果不需要监控可留空 (类型: map) monitor: # 监控进程名称,如“service_a”,平台在发现service_a进程不存在后,会发送告警提醒,不需要监控可留空(类型:string) process_name: "" # 监控端口号,请根据 ports 中的变量设置,不需要监控可留空 (类型: {string}) metric_port: {service_port} --- # 定义该组件安装所需依赖组件名称与版本,如不需其他组件依赖,可留空 (类型: list[map,map..]) dependencies: - name: mysql version: 5.7.31 - name: redis version: 5.0.1 - name: python version: 3.8.3 # 该组件所需最小资源需求 (类型:map) resources: # cpu最小需求 ,1000m 表示 1核 (类型:string) cpu: 1000m # 内存最小需求, 500m 表示 500兆内存 (类型:string) memory: 500m --- # 定义安装组件时所需参数,该参数会传入到 安装脚本中 (类型:list[map,map...]) install: # 传入参数中文描述名称,该名称会在用户安装组件时显示到表单中 (类型: string) - name: "安装目录" # 传入参数key值,会将该key与值 传入到安装脚本中 (类型:string) key: base_dir # 上面key默认值 (类型: stirng) default: "{data_path}/omp_server" # 注: {data_path} 为主机数据目录占位符,请勿使用其他代替 # - name: "JVM设置" # key: jvm # default: "-XX:MaxPermSize=512m -Djava.awt.headless=true" # 程序控制脚本与服务目录的相对路径 (类型:map) control: # 启动脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:string) start: "./bin/omp_server start" # 停止脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:stirng) stop: "./bin/omp_server stop" # 重启脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:stirng) restart: "./bin/omp_server restart" # 重载脚本路径,如没有可留空 (类型:stirng) reload: # 安装脚本路径,必填 (类型:stirng) install: "./scripts/install.py" # 初始化脚本路径,必填 (类型:stirng) init: "./scripts/init.py" ``` ================================================ FILE: doc/changelogs.md ================================================ ## 更新日志 V0.1.0 (2021.11.30) - 新增功能: 【仪表盘】 - 全局状态概览 - 当前异常信息展示 - 各模块状态展示 【主机管理】 - 主机纳管(添加、导入、编辑、维护、删除) - 主机自动监控、告警 【应用商店】 - 组件、应用WEB发布、服务端自动发现 - 组件、应用部署,自动编排解决依赖 【服务管理】 - 服务管理(启动、停止、重启、删除) - 服务监控(监控、日志、告警、自愈) 【应用监控】 - 实时展示处于异常的主机、服务信息,呼应仪表盘的异常清单 - 告警历史记录查看,未读提醒,按添加检索 - 支持监控组件地址自定义,便于对接现有监控平台 【状态巡检】 - 支持主机巡检、组件巡检、深度分析,且支持导出 - 支持定时自动执行巡检任务 【 系统管理】 - 用户账户管理 - 支持全局维护模式,避免人为操作时误报 ================================================ FILE: logs/.gitkeep ================================================ ================================================ FILE: omp_server/app_store/__init__.py ================================================ ================================================ FILE: omp_server/app_store/admin.py ================================================ # from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/app_store/app_store_filters.py ================================================ """ 应用商店相关过滤器 """ import django_filters from django_filters.rest_framework import FilterSet from db_models.models import ( Labels, ApplicationHub, ProductHub, UploadPackageHistory, MainInstallHistory, Service ) class LabelFilter(FilterSet): """ 标签过滤类 """ label_type = django_filters.CharFilter( help_text="标签类型: 0-组件 1-应用", field_name="label_type", lookup_expr="exact") class Meta: model = Labels fields = ("label_type",) class ComponentFilter(FilterSet): """ 基础组件过滤类 """ app_name = django_filters.CharFilter( help_text="基础组件名称,模糊匹配", field_name="app_name", lookup_expr="icontains") type = django_filters.CharFilter( help_text="类型名称", field_name="app_labels__label_name", lookup_expr="exact") class Meta: model = ApplicationHub fields = ("app_name", "type") class ServiceFilter(FilterSet): """ 应用服务过滤器类 """ pro_name = django_filters.CharFilter( help_text="应用服务名称,模糊匹配", field_name="pro_name", lookup_expr="icontains") type = django_filters.CharFilter( help_text="类型名称", field_name="pro_labels__label_name", lookup_expr="exact") class Meta: model = ProductHub fields = ("pro_name", "type") class UploadPackageHistoryFilter(FilterSet): """ 发布-安装包校验结果接口 """ operation_uuid = django_filters.CharFilter( help_text="operation_uuid,查询", field_name="operation_uuid", lookup_expr="exact") class Meta: model = UploadPackageHistory fields = ("operation_uuid",) class PublishPackageHistoryFilter(FilterSet): """ 发布-安装包校验结果接口 """ operation_uuid = django_filters.CharFilter( help_text="operation_uuid,查询", field_name="operation_uuid", lookup_expr="exact") class Meta: model = UploadPackageHistory fields = ("operation_uuid",) class ComponentEntranceFilter(FilterSet): """ 基础组件安装入口过滤类 """ app_name = django_filters.CharFilter( help_text="基础组件名称,精确匹配", field_name="app_name", lookup_expr="exact") class Meta: """ 元数据 """ model = ApplicationHub fields = ("app_name", ) class ProductEntranceFilter(FilterSet): """ 基础组件安装入口过滤类 """ pro_name = django_filters.CharFilter( help_text="基础组件名称,精确匹配", field_name="pro_name", lookup_expr="exact") class Meta: """ 元数据 """ model = ProductHub fields = ("pro_name", ) class InstallHistoryFilter(FilterSet): """ 基础组件安装入口过滤类 """ operation_uuid = django_filters.CharFilter( help_text="唯一操作的uuid", field_name="operation_uuid", lookup_expr="exact") class Meta: """ 元数据 """ model = MainInstallHistory fields = ("operation_uuid", ) class ServiceInstallHistoryFilter(FilterSet): """ 基础组件安装入口过滤类 """ id = django_filters.NumberFilter( help_text="服务id", field_name="id", lookup_expr="exact") class Meta: """ 元数据 """ model = Service fields = ("id", ) ================================================ FILE: omp_server/app_store/app_store_serializers.py ================================================ """ 应用商店 """ import json import logging import os import time from django.conf import settings from rest_framework import serializers from rest_framework.serializers import ModelSerializer from rest_framework.exceptions import ValidationError from rest_framework.serializers import Serializer from utils.common.exceptions import OperateError from utils.plugin.public_utils import check_is_ip_address, timedelta_strftime from app_store.tmp_exec_back_task import front_end_verified_init from db_models.models import ( ApplicationHub, ProductHub, UploadPackageHistory, Service, DetailInstallHistory, MainInstallHistory, Product, DeploymentPlan, ExecutionRecord) from db_models import models from app_store.install_utils import ( make_lst_unique, ServiceArgsSerializer, SerDependenceParseUtils, ProDependenceParseUtils, ValidateExistService, ValidateInstallService, CreateInstallPlan ) from utils.parse_config import HADOOP_ROLE logger = logging.getLogger("server") class ComponentListSerializer(ModelSerializer): """ 组件列表序列化器 """ instance_number = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ApplicationHub fields = ("app_name", "app_version", "app_logo", "app_description", "instance_number") def get_instance_number(self, obj): """ 获取组件已安装实例数量 """ return Service.objects.filter( service__app_name=obj.app_name).count() class ServiceListSerializer(ModelSerializer): """ 服务列表序列化器 """ instance_number = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ProductHub fields = ("pro_name", "pro_version", "pro_logo", "pro_description", "instance_number") def get_instance_number(self, obj): """ 获取组件已安装实例数量 """ # return Service.objects.filter( # service__product__pro_name=obj.pro_name).count() return Product.objects.filter(product=obj).count() class UploadPackageSerializer(Serializer): """上传安装包序列化类""" uuid = serializers.CharField( help_text="上传安装包uuid", required=True, error_messages={"required": "必须包含[uuid]字段"} ) operation_user = serializers.CharField( help_text="操作用户", required=True, error_messages={"required": "必须包含[operation_user]字段"} ) file = serializers.FileField( help_text="上传的文件", required=True, error_messages={"required": "必须包含[file]字段"} ) md5 = serializers.CharField( help_text="文件包的md5值", required=True, error_messages={"required": "必须包含[md5]字段"} ) def validate(self, attrs): file = attrs.get("file") file_name = file.name file_size = file.size if not file_name.endswith('.tar') and not file_name.endswith('tar.gz'): raise ValidationError({ "file_name": "上传文件名仅支持.tar或.tar.gz" }) # 文件大小超过4G不支持 if file_size > 4294967296: raise ValidationError({ "file_size": "上传文件大小超过4G" }) return attrs def create(self, validated_data): uuid = validated_data.get("uuid") operation_user = validated_data.get("operation_user") request_file = validated_data.get("file") md5 = validated_data.get("md5") package_name = request_file.name if not request_file: raise OperateError("上传文件为空") destination_dir = os.path.join( settings.PROJECT_DIR, 'package_hub/front_end_verified') upload_obj = UploadPackageHistory( operation_uuid=uuid, operation_user=operation_user, package_name=package_name, package_md5=md5, package_path="verified") upload_obj.save() with open(os.path.join(destination_dir, request_file.name), 'wb+') as f: for chunk in request_file.chunks(): try: f.write(chunk) except Exception: upload_obj.delete() raise OperateError("文件写入过程失败") front_end_verified_init(uuid, operation_user, package_name, upload_obj.id, md5) return validated_data class RemovePackageSerializer(Serializer): """ 移除安装包序列化类 """ uuid = serializers.CharField( help_text="上传安装包uuid", required=True, error_messages={"required": "必须包含[uuid]字段"} ) package_names = serializers.ListField( child=serializers.CharField(), help_text="安装包名称列表", required=True, allow_empty=False, error_messages={"required": "必须包含[package_names]字段"} ) def validate(self, attrs): """ 校验安装包名称 """ operation_uuid = attrs.get("uuid") package_names = attrs.get("package_names") queryset = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names, package_parent__isnull=True, is_deleted=False ) if not queryset.exists() or \ len(queryset) != len(package_names): logger.error(f"remove package error: uuid-{operation_uuid}," f"package_names-{package_names}") raise ValidationError({"uuid": "该 uuid 未找到有效的操作记录"}) attrs["queryset"] = queryset return attrs def create(self, validated_data): """ 上传安装包记录表软删除 """ queryset = validated_data.pop("queryset", None) if queryset is not None: queryset.update(is_deleted=True) return validated_data class ApplicationDetailSerializer(ModelSerializer): # NOQA """ 组件详情序列化器 """ app_instances_info = serializers.SerializerMethodField() app_labels = serializers.SerializerMethodField() app_package_md5 = serializers.SerializerMethodField() app_operation_user = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ApplicationHub fields = ("app_name", "app_version", "app_logo", "app_description", "created", "app_dependence", "app_instances_info", "app_labels", "app_package_md5", "app_operation_user") def get_app_instances_info(self, obj): # NOQA """ 获取服务安装实例信息 """ service_objs = Service.objects.filter(service__id=obj.id) service_list = [] for so in service_objs: service_dict = { "instance_name": so.service_instance_name, "host_ip": so.ip, "service_port": None if not so.service_port else json.loads(so.service_port), "app_version": so.service.app_version, "mode": "单实例", # TODO 后续根据cluster字段是否为空来判断是单实例还是集群模式 "created": so.created } service_list.append(service_dict) return service_list def get_app_labels(self, obj): # NOQA return list(obj.app_labels.all().values_list('label_name', flat=True)) def get_app_package_md5(self, obj): # NOQA md5 = "-" if obj.app_package is not None: md5 = obj.app_package.package_md5 return md5 def get_app_operation_user(self, obj): # NOQA return obj.app_package.operation_user class ProductDetailSerializer(ModelSerializer): # NOQA """ 产品详情序列化器 """ pro_instances_info = serializers.SerializerMethodField() pro_labels = serializers.SerializerMethodField() pro_package_md5 = serializers.SerializerMethodField() pro_operation_user = serializers.SerializerMethodField() pro_services = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ProductHub fields = ("pro_name", "pro_version", "pro_logo", "pro_description", "created", "pro_dependence", "pro_services", "pro_instances_info", "pro_labels", "pro_package_md5", "pro_operation_user") def get_pro_instances_info(self, obj): # NOQA """ 获取服务安装实例信息 """ service_objs = Service.objects.filter( service__product__id=obj.id) service_list = [] for so in service_objs: service_dict = { "instance_name": so.service_instance_name, "version": so.service.product.pro_version, "app_name": so.service.app_name, "app_version": so.service.app_version, "host_ip": so.ip, "service_port": None if not so.service_port else json.loads(so.service_port), "created": so.created } service_list.append(service_dict) return service_list def get_pro_labels(self, obj): # NOQA return list(obj.pro_labels.all().values_list('label_name', flat=True)) def get_pro_package_md5(self, obj): # NOQA md5 = "-" if obj.pro_package is not None: md5 = obj.pro_package.package_md5 return md5 def get_pro_operation_user(self, obj): # NOQA try: return obj.pro_package.operation_user except Exception as e: logger.error(e) logger.error("获取服务包user值失败!") def get_pro_services(self, obj): # NOQA pro_services_list = [] apps = ApplicationHub.objects.filter(product_id=obj.id) if not apps: if not obj.pro_services: return pro_services_list pro_services_list.extend(json.loads(obj.pro_services)) return pro_services_list pro_app_name_list = [] for app in apps: uph = UploadPackageHistory.objects.get(id=app.app_package_id) if not uph: continue app_dict = { "name": app.app_name, "version": app.app_version, "created": time.strftime("%Y-%m-%d %H:%M:%S", time.strptime(str(app.created), "%Y-%m-%d %H:%M:%S.%f")), "md5": uph.package_md5 } pro_services_list.append(app_dict) pro_app_name_list.append(app.app_name) for ps in json.loads(obj.pro_services): if ps.get("name") in pro_app_name_list: continue pro_services_list.append(ps) return pro_services_list class UploadPackageHistorySerializer(serializers.ModelSerializer): """ 操作记录序列化类 """ class Meta: """ 元数据 """ model = UploadPackageHistory fields = ["package_name", "package_status", "error_msg", "operation_uuid"] class PublishPackageHistorySerializer(serializers.ModelSerializer): """ 操作记录序列化类 """ class Meta: """ 元数据 """ model = UploadPackageHistory fields = ["package_name", "package_status", "error_msg", "operation_uuid"] class ExecuteLocalPackageScanSerializer(Serializer): """ 本地安装包扫描执行序列化类 """ pass class ComponentEntranceSerializer(serializers.ModelSerializer): """ 组件安装入口数据序列化 """ app_port = serializers.SerializerMethodField() app_dependence = serializers.SerializerMethodField() app_install_args = serializers.SerializerMethodField() deploy_mode = serializers.SerializerMethodField() process_continue = serializers.SerializerMethodField() process_message = serializers.SerializerMethodField() def get_app_port(self, obj): # NOQA """ 获取服务端口 """ return ServiceArgsSerializer().get_app_port(obj) def get_app_dependence(self, obj): # NOQA """ 解析服务级别的依赖关系 """ return ServiceArgsSerializer().get_app_dependence(obj) def get_app_install_args(self, obj): # NOQA """ 解析服务安装过程中的参数 """ return ServiceArgsSerializer().get_app_install_args(obj) def get_deploy_mode(self, obj): # NOQA """ 解析服务的部署模式 """ return ServiceArgsSerializer().get_deploy_mode(obj) def get_process_continue(self, obj): # NOQA """ 服务能否安装的接口 """ return ServiceArgsSerializer().get_process_continue(obj) def get_process_message(self, obj): # NOQA return ServiceArgsSerializer().get_process_message(obj) class Meta: """ 元数据 """ model = ApplicationHub fields = [ "app_name", "app_version", "app_dependence", "app_port", "app_install_args", "deploy_mode", "process_continue", "process_message" ] class ProductEntranceSerializer(serializers.ModelSerializer): """ 产品、应用安装序列化类 """ pro_services = serializers.SerializerMethodField() pro_dependence = serializers.SerializerMethodField() dependence_services_info = serializers.SerializerMethodField() def get_pro_services(self, obj): # NOQA """ 获取服务列表 """ if not obj.pro_services: return list() ser_lst = json.loads(obj.pro_services) for item in ser_lst: ser_obj = ApplicationHub.objects.filter( app_name=item.get("name"), app_version=item.get("version") ).last() if not ser_obj: item["process_continue"] = False item["process_message"] = f"服务{item.get('name')}未发布" continue item["app_port"] = ServiceArgsSerializer().get_app_port(ser_obj) item["process_continue"] = True item["app_install_args"] = \ ServiceArgsSerializer().get_app_install_args(ser_obj) item["deploy_mode"] = \ ServiceArgsSerializer().get_deploy_mode(ser_obj) item["app_dependence"] = \ ServiceArgsSerializer().get_app_dependence(ser_obj) return ser_lst def get_pro_dependence(self, obj): # NOQA """ 获取产品依赖关系 """ _pro = ProDependenceParseUtils(obj.pro_name, obj.pro_version) _dep = _pro.run_pro() return _dep def get_dependence_services_info(self, obj): # NOQA """ 获取服务所依赖的信息 """ _service_lst = self.get_pro_services(obj=obj) if not _service_lst: return [] _all_dependence_ser_info = list() for item in _service_lst: _ser = SerDependenceParseUtils( item.get("name"), item.get("version")) _el_lst = _ser.run_ser() _all_dependence_ser_info.extend(_el_lst) _all_dependence_ser_info = make_lst_unique( _all_dependence_ser_info, "name", "version") return _all_dependence_ser_info class Meta: """ 元数据 """ model = ProductHub fields = [ "pro_name", "pro_version", "pro_dependence", "pro_services", "dependence_services_info" ] class ExecuteInstallSerializer(Serializer): """ 执行安装时需要解析前端上传的数据的准确性,服务间的关联依赖关系 目标服务器上实际安装的数据信息等内容 """ INSTALL_COMPONENT = 0 INSTALL_PRODUCT = 1 INSTALL_TYPE_CHOICES = ( (INSTALL_COMPONENT, "组件安装"), (INSTALL_PRODUCT, "产品安装") ) install_type = serializers.ChoiceField( choices=INSTALL_TYPE_CHOICES, help_text="选择安装方式: 0-组件; 1-应用", required=True, allow_null=False, allow_blank=False, error_messages={"required": "必须包含[install_type]字段"} ) use_exist_services = serializers.ListField( child=serializers.DictField(), help_text="复用已安装的服务列表,eg: [{'name': 'ser1', 'id': 1}]", required=True, allow_empty=True, error_messages={"required": "必须包含[use_exist_services]字段"} ) install_services = serializers.ListField( child=serializers.DictField(), help_text="需要安装的服务列表,eg: [{'name': 'ser1', 'version': 1}]", required=True, allow_empty=False, error_messages={ "required": "必须包含[install_services]字段", "empty": "必须包含将要安装的服务信息" } ) is_valid_flag = serializers.BooleanField( read_only=True, required=False, help_text="数据准确性校验返回标志" ) is_valid_msg = serializers.CharField( read_only=True, required=False, max_length=4096, help_text="数据准确性校验结果信息" ) operation_uuid = serializers.CharField( read_only=True, required=False, max_length=128, help_text="成功下发部署计划后返回的uuid" ) def validate_use_exist_services(self, data): # NOQA """ 校验已经存在的服务是否准确 :param data: :return: """ if not data: return data return ValidateExistService(data=data).run() def validate_install_services(self, data): # NOQA """ 校验即将安装的服务及参数 :param data: :return: """ return ValidateInstallService(data=data).run() def check_lst_valid(self, lst): # NOQA """ 根据列表、字典格式确定安装参数是否符合要求 :param lst: :return: """ for el in lst: if not isinstance(el, dict): return False if "check_flag" in el and not el["check_flag"]: return False return True def validate(self, attrs): """ 安装校验最终要执行的方法,根据安装参数解析结果决定如下操作: 安装参数解析成功:调用安装参数入库方法 安装参数解析失败:直接返回相关安装参数 :param attrs: :return: """ valid_lst = list() use_exist_services = attrs.get("use_exist_services", []) valid_lst.append(self.check_lst_valid(use_exist_services)) install_services = attrs.get("install_services", []) valid_lst.append(self.check_lst_valid(install_services)) for item in install_services: app_install_args = item.get("app_install_args", []) valid_lst.append(self.check_lst_valid(app_install_args)) app_port = item.get("app_port", []) valid_lst.append(self.check_lst_valid(app_port)) logger.info(f"Check install info res: {valid_lst}") if len(set(valid_lst)) != 1 or valid_lst[0] is False: attrs["is_valid_flag"] = False attrs["is_valid_msg"] = "数据校验出错" return attrs # 数据入库逻辑 _create_data_obj = CreateInstallPlan(install_data=attrs) flag, msg = _create_data_obj.run() if not flag: attrs["is_valid_flag"] = False attrs["is_valid_msg"] = msg return attrs attrs["is_valid_flag"] = True attrs["is_valid_msg"] = "" attrs["operation_uuid"] = msg return attrs class InstallHistorySerializer(ModelSerializer): """ 安装历史记录序列化类 """ install_status_msg = serializers.CharField( source="get_install_status_display") detail_lst = serializers.SerializerMethodField() def parse_single_obj(self, obj): # NOQA """ 解析单个服务安装记录信息 :param obj: :type obj: DetailInstallHistory :return: """ _status = obj.install_step_status # 拼接日志 _log = "" if obj.send_flag != 0 and obj.send_msg: _log += obj.send_msg if obj.unzip_flag != 0 and obj.unzip_msg: _log += obj.unzip_msg if obj.install_flag != 0 and obj.install_msg: _log += obj.install_msg if obj.init_flag != 0 and obj.init_msg: _log += obj.init_msg if obj.start_flag != 0 and obj.start_msg: _log += obj.start_msg return { "ip": obj.service.ip, "status": _status, "log": _log, "service_name": obj.service.service.app_name, "service_instance_name": obj.service.service_instance_name } def get_detail_lst(self, obj): # NOQA """ 获取安装细节表 :param obj: :return: """ lst = DetailInstallHistory.objects.filter( main_install_history=obj ) return [self.parse_single_obj(el) for el in lst] class Meta: """ 元数据 """ model = MainInstallHistory fields = ( "operation_uuid", "install_status", "install_status_msg", "install_args", "install_log", "detail_lst" ) class ServiceInstallHistorySerializer(ModelSerializer): """ 安装历史记录序列化类 """ install_step_status = serializers.SerializerMethodField() log = serializers.SerializerMethodField() def get_install_step_status(self, obj): """ 获取服务安装状态 :param obj: :return: """ detail_obj = DetailInstallHistory.objects.filter(service=obj).last() return detail_obj.install_step_status def get_log(self, obj): """ 获取服务日志信息 :param obj: :return: """ detail_obj = DetailInstallHistory.objects.filter(service=obj).last() _log = "" if detail_obj.send_flag != 0 and detail_obj.send_msg: _log += detail_obj.send_msg if detail_obj.unzip_flag != 0 and detail_obj.unzip_msg: _log += detail_obj.unzip_msg if detail_obj.install_flag != 0 and detail_obj.install_msg: _log += detail_obj.install_msg if detail_obj.init_flag != 0 and detail_obj.init_msg: _log += detail_obj.init_msg if detail_obj.start_flag != 0 and detail_obj.start_msg: _log += detail_obj.start_msg return _log class Meta: """ 元数据 """ model = Service fields = ( "install_step_status", "log" ) class DeploymentPlanValidateSerializer(Serializer): """ 部署计划服务信息验证序列化类 """ instance_name_ls = serializers.ListField( child=serializers.CharField(), help_text="主机实例名列表", required=True, allow_empty=False, error_messages={"required": "必须包含[instance_name_ls]字段"} ) service_data_ls = serializers.ListField( child=serializers.DictField(), help_text="服务数据列表", required=True, allow_empty=False, error_messages={"required": "必须包含[host_list]字段"} ) def validate(self, attrs): """ 校验主机数据列表 """ instance_name_ls = attrs.get("instance_name_ls") service_data_ls = attrs.get("service_data_ls") result_dict = { "correct": [], "error": [] } logger.info("deployment plan validate start") # 查询所有 application 信息 service_name_ls = list( map(lambda x: x.get("service_name"), service_data_ls)) _queryset = ApplicationHub.objects.filter( app_name__in=service_name_ls, is_release=True) # 所有 application 默认取最新 new_app_id_list = [] for app in _queryset: new_version = _queryset.filter( app_name=app.app_name ).order_by("-created").first().app_version if new_version == app.app_version: new_app_id_list.append(app.id) app_queryset = _queryset.filter( id__in=new_app_id_list, is_release=True ).select_related("product") # 获取 application 对应的 product 信息 app_now = app_queryset.exclude(product__isnull=True) pro_id_list = app_now.values_list("product_id", flat=True).distinct() # 验证 product 的依赖项均已包含 pro_queryset = ProductHub.objects.filter(id__in=pro_id_list) app_target_all = ApplicationHub.objects.filter( product_id__in=pro_id_list) # 考虑到同产品下会有同名服务情况,做去重处理,按照时间版本号取最新 app_target = [] for pro in pro_queryset: for ser in json.loads(pro.pro_services): app_target.append( app_target_all.filter( app_name=ser["name"], app_version=ser["version"] ).last() ) # 无依赖项则跳过 if not pro.pro_dependence: continue dependence_list = json.loads(pro.pro_dependence) # 校验依赖项的指定版本是否存在 for dependence in dependence_list: name = dependence.get("name") # version = dependence.get("version") pro_obj = pro_queryset.filter( pro_name=name).order_by("-created").first() # if not pro_obj or not pro_obj.pro_version.startswith(version): if not pro_obj: result_dict["error"].append({ "row": -2, "instance_name": "待补充", "service_name": "待补充", "validate_error": f"产品 {pro.pro_name}-{pro.pro_version} " f"缺失依赖产品 {name}" }) # 验证所有 product 下的 application 都已经包含 app_target_all = ApplicationHub.objects.filter( product_id__in=pro_id_list) # 所有 affinity 为 tengine 字段 (Web 服务),不参与比较 now_set = set(filter( lambda x: x.extend_fields.get("affinity") != "tengine", app_now)) target_set = set(filter( lambda x: x.extend_fields.get("affinity") != "tengine", app_target)) diff_set = target_set - now_set # 存在遗漏的 application if diff_set: for app in diff_set: result_dict["error"].append({ "row": -1, "instance_name": "待补充", "service_name": f"{app.app_name}", "validate_error": f"产品 {app.product.pro_name} " f"缺失依赖服务 {app.app_name}" }) # 验证所有 application 的依赖项均已包含 base_env_queryset = ApplicationHub.objects.filter(is_base_env=True) for app in app_queryset: if not app.app_dependence: continue dependence_list = json.loads(app.app_dependence) # 校验依赖项的指定版本是否存在 for dependence in dependence_list: name = dependence.get("name") # version = dependence.get("version") app_obj = app_queryset.filter( app_name=name).order_by("-created").first() # if not app_obj or not app_obj.app_version.startswith(version): if not app_obj: # 如果为 base_env 则跳过 # if base_env_queryset.filter( # app_name=name, app_version=version # ).exists(): if base_env_queryset.filter(app_name=name).exists(): continue result_dict["error"].append({ "row": -2, "instance_name": "待补充", "service_name": f"{name}", "validate_error": f"服务 {app.app_name}-{app.app_version} " f"缺失依赖服务 {name}" }) # hadoop 实例列表、角色集合 hadoop_instance_ls = [] hadoop_role_set = set() for service_data in service_data_ls: # 校验主机数据是否已经存在 if service_data.get("instance_name") not in instance_name_ls: service_data["validate_error"] = "主机不在表格中" result_dict["error"].append(service_data) continue # 校验服务是否存在 app_name = service_data.get("service_name", "unKnow") if not app_queryset.filter( app_name=app_name ).order_by("-created").exists(): service_data["validate_error"] = f"{app_name}服务不在应用商店中" result_dict["error"].append(service_data) continue # 如果含 vip 字段,校验是否为 IP 格式 if service_data.get("vip"): is_valid, _ = check_is_ip_address( service_data.get("vip")) if not is_valid: service_data["validate_error"] = "虚拟IP不合法" result_dict["error"].append(service_data) continue # 如果含 role 字段,校验是否含中文逗号 if service_data.get("role") and \ "," in service_data.get("role"): service_data["validate_error"] = "角色请用英文逗号分隔" result_dict["error"].append(service_data) continue # 当服务名为 hadoop 时,记录 hadoop 实例列表、角色集合 if app_name == "hadoop": hadoop_instance_ls.append(service_data) if service_data.get("role"): hadoop_role_set = hadoop_role_set | set( service_data.get("role").split(",")) continue result_dict["correct"].append(service_data) # 如果存在 hadoop 实例,则校验角色 if hadoop_instance_ls: key_name = "single" if len(hadoop_instance_ls) > 1: key_name = "cluster" diff = set(HADOOP_ROLE.get(key_name)) - hadoop_role_set if diff: for hadoop_instance in hadoop_instance_ls: hadoop_instance["validate_error"] = f"缺少角色{','.join(diff)}" result_dict["error"].append(hadoop_instance) else: for hadoop_instance in hadoop_instance_ls: result_dict["correct"].append(hadoop_instance) # 按照 row 行号对列表进行排序 for v in result_dict.values(): if len(v) > 0: v.sort(key=lambda x: x.get("row", 999)) attrs["result_dict"] = result_dict logger.info("deployment plan validate end") return attrs class DeploymentImportSerializer(Serializer): """ 部署计划导入序列化类 """ instance_info_ls = serializers.ListField( child=serializers.DictField(), help_text="主机信息列表", required=True, allow_empty=False, error_messages={"required": "必须包含[instance_info_ls]字段"} ) service_data_ls = serializers.ListField( child=serializers.DictField(), help_text="服务数据列表", required=True, allow_empty=False, error_messages={"required": "必须包含[host_list]字段"} ) operation_uuid = serializers.CharField( max_length=64, help_text="唯一操作id", required=False ) class DeploymentPlanListSerializer(ModelSerializer): """ 部署计划列表序列化类 """ class Meta: """ 元数据 """ model = DeploymentPlan fields = "__all__" class ExecutionRecordSerializer(ModelSerializer): state_display = serializers.CharField(source="get_state_display") can_rollback = serializers.SerializerMethodField() duration = serializers.SerializerMethodField() def get_can_rollback(self, obj): if obj.module != "UpgradeHistory": return False module_obj = getattr(models, obj.module).objects.get( id=int(obj.module_id) ) return module_obj.can_roll_back def get_duration(self, obj): if not obj.end_time: return "-" return timedelta_strftime(obj.end_time - obj.created) class Meta: model = ExecutionRecord fields = ("id", "operator", "count", "state", "state_display", "can_rollback", "duration", "created", "end_time", "module", "module_id") class ProductCompositionSerializer(ModelSerializer): pro_ser_others = serializers.SerializerMethodField() pro_services = serializers.CharField( # child=serializers.DictField(), help_text="产品包含服务列表", required=True, error_messages={"required": "必须包含[pro_services]字段"} ) pro_name = serializers.CharField(help_text="产品名称", required=True, error_messages={"required": "请填写产品名称"}) pro_version = serializers.CharField(help_text="产品版本", required=True, error_messages={"required": "请填写产品版本"}) def get_pro_ser_others(self, obj, **kwargs): res_list = [] all_apps_set, all_true_apps_set = set(), set() if obj.applicationhub_set.exists(): all_apps = obj.applicationhub_set.values_list("app_name", "app_version") for app in all_apps: all_apps_set.add(",".join(app)) pro_ser = kwargs.get('pro_ser') pro_services = pro_ser if pro_ser else json.loads(obj.pro_services) for t_app in pro_services: all_true_apps_set.add(f"{t_app['name']},{t_app['version']}") # 一部分用作校验用 if pro_ser: return all_true_apps_set - all_apps_set for r_app in all_apps_set - all_true_apps_set: r_app_ls = r_app.split(",") res_list.append( { "name": r_app_ls[0], "version": r_app_ls[1], } ) return res_list def validate_pro_services(self, pro_services): pro_services = json.loads(pro_services) if not isinstance(pro_services, list): raise ValidationError({ "pro_services": "pro_services必须是list" }) pro_ser_len = len(pro_services) ser_name = {} for app in pro_services: ser_name[app.get('name', "")] = app.get('version', '') if len(ser_name) != pro_ser_len: raise ValidationError({ "pro_services": "产品内服务名称需保证唯一或字段传递异常" }) return pro_services def validate(self, attrs): pro_name = attrs.get("pro_name") pro_version = attrs.get("pro_version") pro_obj = ProductHub.objects.filter(pro_name=pro_name, pro_version=pro_version).first() if not pro_obj: raise ValidationError({ "pro_services": "请填写正确的产品名称和版本" }) diff_ser = self.get_pro_ser_others(pro_obj, pro_ser=attrs.get("pro_services")) if diff_ser: raise ValidationError({ "pro_services": f"存在不归属于当前产品的服务{diff_ser}" }) return attrs class Meta: model = ProductHub fields = ("pro_name", "pro_version", "pro_services", "pro_ser_others") class DeleteComponentSerializer(ModelSerializer): """ 基础组件序列化 """ name = serializers.SerializerMethodField() versions = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ApplicationHub fields = ("name", "versions") def get_name(self, obj): return obj.app_name def get_versions(self, obj): return [obj.app_version] class DeleteProDuctSerializer(ModelSerializer): """ 产品序列化 """ name = serializers.SerializerMethodField() versions = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ProductHub fields = ("name", "versions") def get_name(self, obj): return f"{obj.pro_name}|{obj.pro_version}" def get_versions(self, obj): app_ls = [] app_values = ApplicationHub.objects.filter( product=obj).values_list("app_name", "app_version") for app in app_values: app_ls.append(f"{app[0]}|{app[1]}") return app_ls ================================================ FILE: omp_server/app_store/apps.py ================================================ from django.apps import AppConfig class AppStoreConfig(AppConfig): name = 'app_store' ================================================ FILE: omp_server/app_store/cmd_install_utils.py ================================================ # -*- coding: utf-8 -*- # Project: cmd_install_utils # Author: jon.liu@yunzhihui.com # Create time: 2022-01-07 15:10 # IDE: PyCharm # Version: 1.0 # Introduction: import os import xlrd class ReadDeploymentExcel(object): """ 读取部署表格使用类 """ def __init__(self, excel_path): """ :param excel_path: 表格文件绝对路径 """ self.excel_path = excel_path @staticmethod def _read(start_row=1, keys=None, table=None): """ 真正读取表格数据,返回tuple (True, list) or (False, str) :param start_row: :param keys: :param table: :return: """ # 获取总行数 总列数 row_num = table.nrows col_num = table.ncols _res = [] # 这是第一行数据,作为字典的key值 key = table.row_values(0) if row_num <= start_row: return False, "所需要数据行数下无数据" # 读取行数的控制逻辑 for i in range(row_num - 1): # 行数据字典 row_data = {} # 如果当前获取到的数据的行数小于想要的行数时,则跳过,不留存 if i < start_row: continue values = table.row_values(i + 1) # 循环每列获取数据 for x in range(col_num): _key = None if key[x] not in keys: continue _key = keys[key[x]] # 提取真正的值 value = int(values[x]) if isinstance(values[x], float) \ else values[x] row_data[_key] = str(value).strip() row_data["row"] = i + 1 if not row_data: continue # 把字典加到列表中 _res.append(row_data) return _res def read_host_info(self, table): """ 获取节点信息 :param table: 节点信息页对象 :return: """ keys_map = { "实例名[必填]": "instance_name", "IP[必填]": "ip", "端口[必填]": "port", "用户名[必填]": "username", "密码[必填]": "password", "数据分区[必填]": "data_folder", "操作系统[必填]": "operate_system", "是否执行初始化": "init_host", "运行用户": "run_user", "时间同步服务器": "ntpd_server" } return self._read(start_row=4, keys=keys_map, table=table) def read_service_info(self, table): """ 获取服务分布表格信息 :param table: 服务分布页对象 :return: """ keys_map = { "主机实例名[必填]": "instance_name", "服务名[必填]": "service_name", "运行内存": "memory", "虚拟IP": "vip", "角色": "role", "模式": "mode" } return self._read(start_row=3, keys=keys_map, table=table) def read_excel(self): """ 读取表格数据 :return: """ if not os.path.exists(self.excel_path) or \ not os.path.isfile(self.excel_path): return False, f"无法找到此文件: {self.excel_path}" # 打开excel表,填写路径 book = xlrd.open_workbook(self.excel_path) # 找到sheet页 host_table = book.sheet_by_name("节点信息") host_info = self.read_host_info(table=host_table) for item in host_info: if item["username"] == "root": item["init_host"] = True else: item["init_host"] = False if "ntpd_server" in item and item["ntpd_server"] != "": item["use_ntpd"] = True else: item["use_ntpd"] = False item.pop("ntpd_server", "") service_table = book.sheet_by_name("服务分布") service_info = self.read_service_info(table=service_table) return True, { "host": host_info, "service": { "instance_name_ls": [el["instance_name"] for el in host_info], "service_data_ls": service_info } } ================================================ FILE: omp_server/app_store/deploy_mode_utils/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jon.liu@yunzhihui.com # Create time: 2021-11-16 16:44 # IDE: PyCharm # Version: 1.0 # Introduction: """ 服务部署模式 """ from app_store.deploy_mode_utils.mysql import MysqlUtils # from app_store.deploy_mode_utils.normal import NormalUtils from app_store.deploy_mode_utils.odd_num import OddNumUtils # from app_store.deploy_mode_utils.even_num import EvenNumUtils from app_store.deploy_mode_utils.tengine import TengineUtils from app_store.deploy_mode_utils.rocketmq import RocketmqUtils SERVICE_MAP = { "mysql": MysqlUtils, "zookeeper": OddNumUtils, "kafka": OddNumUtils, "nacos": OddNumUtils, "tengine": TengineUtils, "rocketmq": RocketmqUtils } ================================================ FILE: omp_server/app_store/deploy_mode_utils/base.py ================================================ # -*- coding: utf-8 -*- # Project: base # Author: jon.liu@yunzhihui.com # Create time: 2021-11-16 17:10 # IDE: PyCharm # Version: 1.0 # Introduction: class BaseUtils(object): """ 部署模式基础类 """ def __init__(self, host_num, high_availability): """ :param host_num: 主机数量 :type host_num: int :param high_availability: 是否使用高可用 :type high_availability: bool """ self.host_num = host_num self.high_availability = high_availability def get(self): """ 获取部署模式 :return: """ raise NotImplementedError(f"{self}必须实现get方法!") def check(self, mode): """ 校验部署模式 :param mode: 部署模式 :return: """ raise NotImplementedError(f"{self}必须实现check方法!") ================================================ FILE: omp_server/app_store/deploy_mode_utils/even_num.py ================================================ # -*- coding: utf-8 -*- # Project: even_num # Author: jon.liu@yunzhihui.com # Create time: 2021-11-23 15:55 # IDE: PyCharm # Version: 1.0 # Introduction: """ 偶数服务集群部署模式 偶数服务集群最小为4 """ from app_store.deploy_mode_utils.base import BaseUtils class EvenNumUtils(BaseUtils): """ 偶数集群控制 """ def get(self): """ 当服务集群为偶数个时,返回部署模式 :return: """ if self.host_num < 4: return { "default": 1, "step": 0 } return { "default": 4, "step": 2 } def check(self, mode): """ 检查服务集群部署规则 TODO 待完善 :param mode: :type mode: int :return: """ if mode > self.host_num: return False if self.host_num < 4 and mode > 1: return False return True ================================================ FILE: omp_server/app_store/deploy_mode_utils/mysql.py ================================================ # -*- coding: utf-8 -*- # Project: mysql # Author: jon.liu@yunzhihui.com # Create time: 2021-11-16 16:45 # IDE: PyCharm # Version: 1.0 # Introduction: """ mysql部署模式工具 """ from app_store.deploy_mode_utils.base import BaseUtils class MysqlUtils(BaseUtils): """ MySQL的部署模式 """ def get(self): """ 获取mysql的部署模式 :return: """ if self.host_num == 1: return [ { "key": "single", "name": "单实例" } ] elif self.high_availability and self.host_num >= 2: return [ { "key": "master-slave", "name": "主从" }, { "key": "master-master", "name": "主主(vip)" }, { "key": "single", "name": "单实例" } ] return [ { "key": "single", "name": "单实例" }, { "key": "master-slave", "name": "主从" }, { "key": "master-master", "name": "主主(vip)" } ] def check(self, mode): """ 检查部署模式是否符合要求 :param mode: 部署模式 :type mode: str :return: """ if self.host_num == 1: if mode == "single": return True return False if self.host_num >= 2 and \ mode in ("single", "master-slave", "master-master"): return True return False ================================================ FILE: omp_server/app_store/deploy_mode_utils/normal.py ================================================ # -*- coding: utf-8 -*- # Project: normal # Author: jon.liu@yunzhihui.com # Create time: 2021-11-23 16:06 # IDE: PyCharm # Version: 1.0 # Introduction: """ 普通服务的集群模式 起始为1 步长为1 """ from app_store.deploy_mode_utils.base import BaseUtils class NormalUtils(BaseUtils): """ 普通集群控制 """ def get(self): """ 普通服务,返回部署模式 :return: """ if self.host_num == 1: return { "default": 1, "step": 0 } return { "default": 1, "step": 1 } def check(self, mode): """ 检查服务集群部署规则 TODO 待完善 :param mode: :type mode: int :return: """ if mode > self.host_num: return False return True ================================================ FILE: omp_server/app_store/deploy_mode_utils/odd_num.py ================================================ # -*- coding: utf-8 -*- # Project: odd_num # Author: jon.liu@yunzhihui.com # Create time: 2021-11-23 15:55 # IDE: PyCharm # Version: 1.0 # Introduction: """ 奇数服务集群部署模式 奇数服务集群最小为3 """ from app_store.deploy_mode_utils.base import BaseUtils class OddNumUtils(BaseUtils): """ 奇数集群控制 """ def get(self): """ 当服务集群为奇数个时,返回部署模式 :return: """ if self.host_num < 3: return { "default": 1, "step": 0 } if not self.high_availability: return { "default": 1, "step": 2 } return { "default": 3, "step": 2 } def check(self, mode): """ 检查服务集群部署规则 TODO 待完善 :param mode: :type mode: int :return: """ if mode > self.host_num: return False if self.host_num < 3 and mode > 1: return False return True ================================================ FILE: omp_server/app_store/deploy_mode_utils/rocketmq.py ================================================ # -*- coding: utf-8 -*- # Project: mysql # Author: jon.liu@yunzhihui.com # Create time: 2021-11-16 16:45 # IDE: PyCharm # Version: 1.0 # Introduction: """ rocketmq部署模式工具 """ from app_store.deploy_mode_utils.base import BaseUtils class RocketmqUtils(BaseUtils): """ Rocket的部署模式 """ def get(self): """ 获取mysql的部署模式 :return: """ if self.host_num == 1: return { "default": 1, "step": 0 } if self.high_availability: if self.host_num >= 2: return { "default": 2, "step": 2 } return { "default": 1, "step": 0 } return { "default": 1, "step": 1 } def check(self, mode): """ 检查部署模式是否符合要求 :param mode: 部署模式 :type mode: str :return: """ return True ================================================ FILE: omp_server/app_store/deploy_mode_utils/tengine.py ================================================ # -*- coding: utf-8 -*- # Project: tengine # Author: jon.liu@yunzhihui.com # Create time: 2021-11-23 16:26 # IDE: PyCharm # Version: 1.0 # Introduction: """ tengine部署模式工具 """ from app_store.deploy_mode_utils.base import BaseUtils class TengineUtils(BaseUtils): """ tengine的部署模式 """ def get(self): """ 获取tengine的部署模式 :return: """ # return { # "default": 1, # "step": 1 # } return [ { "key": "single", "name": "单实例" }, { "key": "master-master", "name": "主主(vip)" } ] def check(self, mode): """ 检查部署模式是否符合要求 :param mode: 部署模式 :type mode: int :return: """ # if mode != 1: # return False return True ================================================ FILE: omp_server/app_store/deploy_role_utils/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jerry.zhang@yunzhihui.com # Create time: 2021-12-15 15:46 # IDE: PyCharm # Version: 1.0 # Introduction: from app_store.deploy_role_utils.hadoop import Hadoop from app_store.deploy_role_utils.redis import Redis from app_store.deploy_role_utils.mysql import Mysql DEPLOY_ROLE_UTILS = { "hadoop": Hadoop, "redis": Redis, "mysql": Mysql } ================================================ FILE: omp_server/app_store/deploy_role_utils/hadoop.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jerry.zhang@yunzhihui.com # Create time: 2021-12-16 10:10 # IDE: PyCharm # Version: 1.0 # Introduction: import logging logger = logging.getLogger("server") class Hadoop(object): @staticmethod def update_service(service_list): """ 分配hadoop服务角色 :param service_list: 服务数据列表 :return: """ need_distribution = [ "namenode,zkfc", "resourcemanager", "namenode,zkfc", "resourcemanager"] base_role = "datanode,nodemanager,journalnode" # worker_role = "datanode,nodemanager" 后期优化分配 if len(service_list) == 1: service_list[0]['roles'] = "namenode,secondarynamenode," \ "datanode,resourcemanager,nodemanager" return service_list for index, i in enumerate(service_list): if i.get('roles'): continue if index == len(service_list) - 1 and len(need_distribution) > 1: i['roles'] = "{0},{1}".format( base_role, ",".join(need_distribution)) elif index == 0 and len(service_list) == 2: i['roles'] = "{0},{1}".format( base_role, ",".join(need_distribution[:2])) need_distribution = need_distribution[2:] elif len(service_list) >= 5 and need_distribution[0] == "namenode,zkfc": role = need_distribution.pop(0) i['roles'] = f"journalnode,{role}" else: role = need_distribution.pop(0) i['roles'] = f"{base_role},{role}" return service_list ================================================ FILE: omp_server/app_store/deploy_role_utils/mysql.py ================================================ # -*- coding: utf-8 -*- # Project: mysql # Author: jon.liu@yunzhihui.com # Create time: 2021-12-21 20:22 # IDE: PyCharm # Version: 1.0 # Introduction: import logging logger = logging.getLogger("server") class Mysql(object): @staticmethod def update_service(service_list): """ 分配mysql的角色 :param service_list: 服务数据列表 :return: """ mysql_index_lst = list() mysql_vip_flag = False for index, item in enumerate(service_list): logger.info(f"{mysql_vip_flag}; {item}") if item.get("name") == "mysql": mysql_index_lst.append(index) if item.get("roles") == "mysql": mysql_vip_flag = True if len(mysql_index_lst) == 1: service_list[mysql_index_lst[0]]["roles"] = "single" elif len(mysql_index_lst) == 2: if not mysql_vip_flag: service_list[mysql_index_lst[0]]["roles"] = "master" service_list[mysql_index_lst[1]]["roles"] = "slave" else: service_list[mysql_index_lst[0]]["roles"] = "master" service_list[mysql_index_lst[1]]["roles"] = "master" return service_list ================================================ FILE: omp_server/app_store/deploy_role_utils/redis.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jerry.zhang@yunzhihui.com # Create time: 2021-12-16 10:10 # IDE: PyCharm # Version: 1.0 # Introduction: import logging logger = logging.getLogger("server") class Redis(object): @staticmethod def update_service(service_list): """ 分配redis服务角色 :param service_list: 服务数据列表 :return: """ if len(service_list) == 1: service_list[0]['roles'] = "master" for index, i in enumerate(service_list): if i.get('roles'): continue if index == 0: i['roles'] = "master" else: i['roles'] = "slave" return service_list ================================================ FILE: omp_server/app_store/high_availability_utils/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jerry.zhang@yunzhihui.com # Create time: 2021-12-15 15:46 # IDE: PyCharm # Version: 1.0 # Introduction: from app_store.high_availability_utils.hadoop import Hadoop HIGH_AVAILABILITY_UTILS = { "hadoop": Hadoop } ================================================ FILE: omp_server/app_store/high_availability_utils/hadoop.py ================================================ import logging import os import json from utils.common.exceptions import GeneralError from db_models.models import ( Service, DetailInstallHistory, Host, ServiceHistory, ClusterInfo ) from django.db.models import F from django.db import transaction from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from utils.plugin.salt_client import SaltClient THREAD_POOL_MAX_WORKERS = 20 logger = logging.getLogger("server") class Hadoop(object): ACTION_LS = ("send", "unzip", "install") hadoop_init = [ ("init", "zkfc"), ("start", "journalnode"), ("format", "namenode"), ("sync", "namenode"), ("start", "zkfc"), ("start", "datanode"), ("start", "resourcemanager"), ("start", "nodemanager") ] def __init__(self, install_exec_obj, detail_list_obj): """ 解析数据 :param install_exec_obj InstallServiceExecutor对象实例 :parm detail_list_obj detail的orm对象列表 :return: """ self.install_obj = install_exec_obj self.detail_list = detail_list_obj self.timeout = 300 self.error = False self.target_set = set() self.count = 0 self.detail_dict = {} # 中间结果,任意一个hadoop_init失败所有的detail对象全部失败 self.port = {} self.base_dir = {} def check_result(self, future_list): for future in as_completed(future_list): is_success, message = future.result() if not is_success: self.install_obj.is_error = True self.error = True break self.count += 1 def init_hadoop(self, detail_obj, target_ip, service_controllers_dict, action): try: # 获取服务初始化脚本绝对路径 salt_client = SaltClient() init_script_path = service_controllers_dict.get("init", "") # 获取 json 文件路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None json_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") cmd_str = f"python {init_script_path} --local_ip {target_ip} " \ f"--data_json {json_path} --action_type {action[0]} --action_object {action[1]}" # 执行初始化 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise GeneralError(message) # 执行成功且 message 有值,则补充至服务日志中 detail_obj.install_msg += \ f"{self.install_obj.now_time()} 初始化脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() return True, "success" except Exception as err: for obj, name in self.detail_dict.items(): logger.error(f"Init Failed -> [{name}]: {err}") obj.init_flag = 3 obj.init_msg += f"{self.install_obj.now_time()} {name} " \ f"初始化服务失败: {err}\n" # 更新安装流程状态为 '失败',服务状态为 '安装失败' obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED obj.save() obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED obj.service.save() # 创建历史记录 self.install_obj.create_history(obj, is_success=False) return False, err def init(self, action, detail_obj_list): """ 初始化服务 """ # 获取初始化使用参数 with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: future_list = [] for detail_obj in detail_obj_list: target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name service_controllers_dict = detail_obj.service.service_controllers if target_ip not in self.target_set: # 中间结果,全部成功后更新状态 self.detail_dict[detail_obj] = service_name self.target_set.add(target_ip) logger.info(f"Init Begin -> [{service_name}]") detail_obj.init_flag = 1 detail_obj.init_msg += f"{self.install_obj.now_time()} {service_name} 开始初始化服务\n" detail_obj.save() future_obj = executor.submit( self.init_hadoop, detail_obj, target_ip, service_controllers_dict, action ) future_list.append(future_obj) self.check_result(future_list) # 安装成功 if self.count == 9: for obj, name in self.detail_dict.items(): logger.info(f"Init Success -> [{name}]") obj.init_flag = 2 obj.init_msg += f"{self.install_obj.now_time()} {name} 成功初始化服务\n" # 完成安装流程,更新状态为 '安装成功' obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS # 创建历史记录 self.install_obj.create_history(obj, is_success=True) return True, "Init Success" def single_service_executor(self, detail_obj): """ 单独服务的安装执行器 :param detail_obj: :type detail_obj: DetailInstallHistory :return: """ # 更改服务状态为安装中状态 detail_obj.service.service_status = Service.SERVICE_STATUS_INSTALLING detail_obj.service.save() # 跳过单个服务的已经成功的单个步骤不再重复执行 for action in self.ACTION_LS: if getattr(detail_obj, f"{action}_flag") == 2: continue _flag, _msg = getattr(self.install_obj, action)(detail_obj) if not _flag: return _flag, _msg return True, "success" def sync_port(self, service_obj): """ 同步要监控的port """ ports = json.loads(service_obj.service_port) role_key = { "namenode_rpc_port": "namenode", "journalnode_rpc_port": "journalnode", "resourcemanager_webapp_port": "resourcemanager", "nodemanager_port": "nodemanager", "datanode_rpc_port": "datanode", "zkfc_rpc_port": "zkfc", "secondarynamenode_rpc_port": "secondarynamenode" } for port in ports: port_key = role_key.get(port.get("key", "")) if port_key: self.port[port_key] = port.get("default", "") def sync_dir(self, obj_list): for detail in obj_list: install_file = detail.service.service_controllers.get( "install", "") if install_file: install_file = install_file.replace("install.py", "hadoop") self.base_dir[detail.service.ip] = install_file def _get_service_port(self, role): port = self.port.get(role, "") port_json = [{ "name": role, "protocol": "TCP", "key": "service_port", "default": port }] return json.dumps(port_json, ensure_ascii=False) def _get_service_controllers(self, ip, role): script_dir = self.base_dir.get(ip, "").split()[0] controllers = { "init": "", "start": f"{script_dir} start {role}", "stop": f"{script_dir} stop {role}", "restart": f"{script_dir} restart {role}", "install": "" } return controllers def _create_service(self, role, detail_obj): ip = detail_obj.service.ip instance_name = role + "_" + "_".join(ip.split(".")[2:]) # if "secondarynamenode" in detail_obj.service.service_role: # cluster = ClusterInfo.objects.get_or_create( # cluster_service_name="hadoop", # cluster_name=detail_obj.service.service_instance_name, # )[0] # else: # cluster = detail_obj.service.cluster service_obj = Service.objects.create( ip=ip, service_instance_name=instance_name, service_status=0, service=detail_obj.service.service, service_port=self._get_service_port(role), service_controllers=self._get_service_controllers(ip, role), # cluster=cluster, env=detail_obj.service.env, service_split=2 ) # TODO 操作用户 ServiceHistory.objects.create( username="admin", description="安装服务", result="success", service=service_obj) Host.objects.filter(ip=ip).update( service_num=F("service_num") + 1) return service_obj def _create_detail(self, service_obj, detail_obj): """ 创建detail表 """ status = DetailInstallHistory.INSTALL_STATUS_SUCCESS DetailInstallHistory.objects.create( service=service_obj, main_install_history=detail_obj.main_install_history, install_step_status=status, send_flag=status, unzip_flag=status, install_flag=status, init_flag=status, start_flag=status, post_action_flag=status, install_detail_args=detail_obj.install_detail_args ) def high_thread_executor(self): """ 多线程执行器 """ logger.info(f"Start thread poll executor for {self.detail_list}") with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: _future_list = [] for detail_obj in self.detail_list: # 更新单条安装记录的状态 detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_INSTALLING detail_obj.save() future_obj = executor.submit( self.single_service_executor, detail_obj ) _future_list.append(future_obj) self.check_result(_future_list) for action in self.hadoop_init: if not self.error: self.init(action, self.detail_list) if not self.error: # 创建表数据 with transaction.atomic(): self.sync_port(self.detail_list[0].service) self.sync_dir(self.detail_list) for obj in self.detail_list: host_ip = obj.service.ip roles_name = obj.service.service_role for role in roles_name.split(","): ser_obj = self._create_service(role, obj) self._create_detail(ser_obj, obj) obj.service.service_status = Service.SERVICE_STATUS_UNKNOWN obj.service.service_split = 1 obj.service.save() # obj.service.delete() # 更新拆分前服务detail状态 obj.install_step_status = DetailInstallHistory.INSTALL_STATUS_SUCCESS obj.save() Host.objects.filter(ip=host_ip).update( service_num=F("service_num") - 1) logger.info("Finish thread poll executor!") ================================================ FILE: omp_server/app_store/install_exec.py ================================================ """ 安装执行器 前提:所有的公共组件都由OMP进行安装处理 目标:根据数据库中给出的安装记录进行服务安装 """ import copy import json import os import time import queue import logging import re from concurrent.futures import ( ThreadPoolExecutor, as_completed ) # from django.db.models import F from django.conf import settings from app_store.high_availability_utils import HIGH_AVAILABILITY_UTILS from db_models.models import ( Host, Service, HostOperateLog, ServiceHistory, MainInstallHistory, DetailInstallHistory, ApplicationHub, PreInstallHistory, PostInstallHistory ) from utils.plugin.salt_client import SaltClient from utils.parse_config import BASIC_ORDER from utils.parse_config import THREAD_POOL_MAX_WORKERS from utils.common.exceptions import GeneralError from omp_server.settings import PROJECT_DIR from app_store.post_install_utils import POST_INSTALL_SERVICE from app_store.service_splitting import service_splitting UNZIP_CONCURRENT_ONE_HOST = 3 OPENSSL_VERSION_LEVEL = 102 DOIM_APP_NAME = 'doim' logger = logging.getLogger("server") class InstallServiceExecutor: """ 安装服务执行器 """ ACTION_LS = ("send", "unzip", "install", "init", "start") def __init__(self, main_id, username, timeout=300): self.main_id = main_id self.username = username self.timeout = timeout # 安装中是否发生错误,用于流程控制 self.is_error = False # 控制安装过程中单主机上的安装包解压并发数 TODO 暂时使用阻塞等待方式进行处理!! self.unzip_concurrent_controller = dict() def parse_origin_data(self, json_source_path): """ 解析数据 :param json_source_path: :return: """ _file_name = os.path.join(PROJECT_DIR, "package_hub", json_source_path) with open(_file_name, "r") as fp: content = json.loads(fp.read()) if not isinstance(content, list): raise GeneralError("json文件不符合格式要求!") ip_user_map = dict() for item in content: if item["ip"] not in ip_user_map: ip_user_map[item["ip"]] = list() if "install_args" not in item: continue for el in item["install_args"]: if el.get("key") == "run_user" and el.get("default"): ip_user_map[item["ip"]].append(el.get("default")) break return ip_user_map def set_hostname_analysis(self, ips_data, ip, salt_client): test_write_host_func = "cat /tmp/init_host.py | grep write_hostname" flag, msg = salt_client.cmd( target=ip, command=test_write_host_func, timeout=60 ) logger.info(f"测试主机[{ip}]init_host.py脚本write_host功能,结果 {msg}") if not flag or "write_hostname" not in msg: is_success, message = salt_client.cp_file( target=ip, source_path="_modules/init_host.py", target_path="/tmp/init_host.py" ) logger.info(f"主机[{ip}]发送init_host.py脚本成功!") if not is_success: return f"{self.now_time()} 执行主机名解析失败!请手动添加集群主机名解析!\n" host_data = [] for k, v in ips_data.items(): if not v.get("host_name"): logger.error(f"未获取到主机[{ip}]的主机名,添加主机名解析失败!") return f"未获取到主机[{ip}]的主机名,添加主机名解析失败!" \ f"请重启主机agent重试或手动添加主机名解析" host_data.append({"ip": k, "hostname": v.get("host_name")}) hosts_data = json.dumps(host_data, separators=(',', ':')) # 直接用sudo执行,报错即进行警告 write_host = f"sudo python /tmp/init_host.py write_hostname '{hosts_data}'" flag, msg = salt_client.cmd( target=ip, command=write_host, timeout=60 ) if not flag: return f"{self.now_time()} 执行主机名解析失败!请手动添加集群主机名解析!\n" return f"{self.now_time()} 执行主机名解析成功!\n" def _execute_pre_install( self, ips_data, key, value, main_obj, json_source_path, salt_client, pre_install_obj ): """ :param ips_data: :param key: :param value: :param main_obj: :param json_source_path: :param salt_client: :param pre_install_obj: :return: """ try: message = self.set_hostname_analysis(ips_data, key, salt_client) except Exception as e: logger.error(f"执行主机名解析报错:{str(e)}") message = f"{self.now_time()} 执行主机名解析失败!请手动添加集群主机名解析!\n" pre_install_obj.install_log += message pre_install_obj.save() json_target_path = os.path.join( ips_data[key].get("data_folder", "/data"), f"omp_packages/{main_obj.operation_uuid}.json") pre_install_obj.install_log += f"{self.now_time()} 开始发送json文件\n" pre_install_obj.save() # 发送 json 文件 is_success, message = salt_client.cp_file( target=key, source_path=json_source_path, target_path=json_target_path) if not is_success: pre_install_obj.install_log += \ f"{self.now_time()} 发送json文件失败: {message}\n" pre_install_obj.install_flag = 3 pre_install_obj.save() raise GeneralError(f"发送 json 文件失败: {message}") for el in set(value): _cmd = f"id {el} || useradd -s /bin/bash {el}" pre_install_obj.install_log += \ f"{self.now_time()} 开始执行创建用户命令: {_cmd}\n" pre_install_obj.save() flag, msg = salt_client.cmd( target=key, command=_cmd, timeout=60 ) logger.info(f"为主机 [{key}] 创建用户 [{el}] 结果 {msg}") if not flag: pre_install_obj.install_log += \ f"{self.now_time()} 创建用户命令执行失败: {msg}\n" pre_install_obj.install_flag = 3 pre_install_obj.save() raise GeneralError(f"为主机 [{key}] 创建用户 [{el}] 失败!") # 适配doim doim_queryset = DetailInstallHistory.objects.select_related( "service", "service__service" ).filter(main_install_history_id=self.main_id, service__service__app_name__iexact=DOIM_APP_NAME).exclude( install_step_status=DetailInstallHistory.INSTALL_STATUS_SUCCESS) doim_ips = set([item.service.ip for item in doim_queryset]) pre_install_obj.install_log += \ f"{self.now_time()} 升级openssl version参数位:[doim_queryset] == {doim_queryset};[key]={key}; [doim_ips]={doim_ips}\n" pre_install_obj.save() if doim_queryset and key in doim_ips: # 1.run_user 必须为root if "root" not in value: pre_install_obj.install_log += \ f"{self.now_time()} 安装doim执行失败,用户[{value}]为非root\n" pre_install_obj.install_flag = 3 pre_install_obj.save() raise GeneralError( f"主机 [{key}] 的run_user[{value}]为非root,不支持doim安装") # 2.获取openssl版本号 openssl_version_cmd = "openssl version" ssl_version = self.get_openssl_version_from_cmd(ip=key, salt_client=salt_client, cmd_str=openssl_version_cmd) if ssl_version < OPENSSL_VERSION_LEVEL: pre_install_obj.install_log += f"{self.now_time()} 当前系统的openssl version 为{ssl_version}; 开始升级openssl version\n" pre_install_obj.save() upgrade_flag, upgrade_msg = self.upgrade_openssl( ip=key, salt_client=salt_client) if not upgrade_flag: pre_install_obj.install_log += \ f"{self.now_time()} 升级openssl version执行失败: {upgrade_msg}\n" pre_install_obj.install_flag = 3 pre_install_obj.save() raise GeneralError(f"为主机 [{key}] 升级openssl version执行失败!") pre_install_obj.install_log += f"{self.now_time()} 当前系统的openssl version成功升级\n" pre_install_obj.save() pre_install_obj.install_log += \ f"{self.now_time()} 前置安装操作完成\n" pre_install_obj.install_flag = 2 pre_install_obj.save() def create_user_and_send_json(self, main_obj): """ 下发json文件并创建run_user用户 :return: """ # 获取 json 文件路径 salt_client = SaltClient() json_source_path = os.path.join( "data_files", f"{main_obj.operation_uuid}.json") ips_data = { host["ip"]: host for host in list( Host.objects.all().values( "ip", "data_folder", "username", "host_name" ) ) } ip_user_map = self.parse_origin_data(json_source_path) for key, value in ip_user_map.items(): pre_install_obj = PreInstallHistory.objects.filter( main_install_history=main_obj, ip=key ).last() if not pre_install_obj or pre_install_obj.install_flag == 2: continue try: pre_install_obj.install_flag = 1 pre_install_obj.install_log += \ f"{self.now_time()} 开始执行前置安装操作\n" pre_install_obj.save() self._execute_pre_install( ips_data, key, value, main_obj, json_source_path, salt_client, pre_install_obj ) except GeneralError as e: pre_install_obj.install_log += \ f"{self.now_time()} 前置安装操作失败: {str(e)}\n" pre_install_obj.install_flag = 3 pre_install_obj.save() @staticmethod def now_time(): """ 当前时间格式 """ return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def create_history(self, detail_obj, is_success=True): """ 创建历史记录 """ target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name # 写入主机历史记录、服务历史记录 target_host = Host.objects.filter(ip=target_ip).first() HostOperateLog.objects.create( username=self.username, description=f"安装服务 [{service_name}]", result="success" if is_success else "failed", host=target_host) ServiceHistory.objects.create( username=self.username, description="安装服务", result="success" if is_success else "failed", service=detail_obj.service) # 主机服务数量+1 # 屏蔽安装完成后服务数 +1 逻辑 # if is_success: # Host.objects.filter(ip=target_ip).update( # service_num=F("service_num") + 1) def send(self, detail_obj): """ 发送服务包 """ # 获取发送使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name package_name = detail_obj.service.service.app_package.package_name # 更新状态为 '发送中',记录日志 logger.info(f"Send Begin -> [{service_name}] package [{package_name}]") detail_obj.send_flag = 1 detail_obj.send_msg += f"{self.now_time()} {service_name} 开始发送服务包\n" detail_obj.save() try: # 获取目标路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None # 获取 json 文件路径 已将json发送前置 jon.liu # json_source_path = os.path.join( # "data_files", # f"{detail_obj.main_install_history.operation_uuid}.json") # json_target_path = os.path.join( # target_host.data_folder, "omp_packages", # f"{detail_obj.main_install_history.operation_uuid}.json") # # # 发送 json 文件 # is_success, message = salt_client.cp_file( # target=target_ip, # source_path=json_source_path, # target_path=json_target_path) # if not is_success: # raise GeneralError(f"发送 json 文件失败: {message}") # 获取服务包路径 source_path = os.path.join( detail_obj.service.service.app_package.package_path, package_name) target_path = os.path.join( target_host.data_folder, "omp_packages", package_name) # 发送服务包 is_success, message = salt_client.cp_file( target=target_ip, source_path=source_path, target_path=target_path) if not is_success: raise GeneralError(message) except Exception as err: logger.error(f"Send Failed -> [{service_name}]: {err}") detail_obj.send_flag = 3 detail_obj.send_msg += f"{self.now_time()} {service_name} " \ f"发送服务包失败: {err}\n" detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 发送成功 logger.info( f"Send Success -> [{service_name}] package [{package_name}]") detail_obj.send_flag = 2 detail_obj.send_msg += f"{self.now_time()} {service_name} 成功发送服务包\n" detail_obj.save() return True, "Send Success" def unzip(self, detail_obj): """ 解压服务包 """ # 获取解压使用参数 target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name package_name = detail_obj.service.service.app_package.package_name salt_client = SaltClient() # 更新状态为 '解压中',记录日志 logger.info( f"Unzip Begin -> [{service_name}] package [{package_name}]") detail_obj.unzip_flag = 1 detail_obj.unzip_msg += f"{self.now_time()} {service_name} 开始解压服务包\n" detail_obj.save() try: # 控制单主机上的服务包解压操作,向控制队列中添加一项 # 如果能加入成功则继续,否则等待其加入成功 self.unzip_concurrent_controller[target_ip].put(service_name) # 解析获取目录 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None package_path = os.path.join( target_host.data_folder, "omp_packages", package_name) # 获取解压目标路径 detail_args = detail_obj.install_detail_args assert detail_args is not None app_name = detail_args.get("name", None) assert app_name is not None target_path = None for info in detail_args.get("install_args", []): if info.get("key", "") == "base_dir": target_path = info.get("default") break if target_path is None: raise GeneralError("未获取到解压目标路径") # 切分判断路径 path_ls = os.path.split(target_path) # 创建服务目录,解压服务包 if path_ls[1] == app_name: _target_path = path_ls[0] test_path_cmd_str = f"(test -d {_target_path} || mkdir -p {_target_path}) && " \ f"tar -xmf {package_path} -C {_target_path}" else: # 当路径结尾与服务名不一致时 _target_path = path_ls[0] real_path = os.path.join(_target_path, app_name) test_path_cmd_str = f"(test -d {_target_path} || mkdir -p {_target_path}) && " \ f"tar -xmf {package_path} -C {_target_path} && mv {real_path} {target_path}/" is_success, message = salt_client.cmd( target=target_ip, command=test_path_cmd_str, timeout=self.timeout) if not is_success: raise GeneralError(message) except Exception as err: # 解压流程运行完成后报错,释放资源 self.unzip_concurrent_controller[target_ip].get() logger.error(f"Unzip Failed -> [{service_name}]: {err}") detail_obj.unzip_flag = 3 detail_obj.unzip_msg += \ f"{self.now_time()} {service_name} " \ f"解压服务包失败: {err}\n" detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 解压流程运行完成后报错,释放资源 self.unzip_concurrent_controller[target_ip].get() # 解压成功 logger.info( f"Unzip Success -> [{service_name}] package [{package_name}]") detail_obj.unzip_flag = 2 detail_obj.unzip_msg += \ f"{self.now_time()} {service_name} 成功解压服务包\n" detail_obj.save() return True, "Unzip Success" def install(self, detail_obj): """ 安装服务 """ # 获取安装使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name app_name = detail_obj.service.service.app_name # edit by jon.liu service_controllers 为json字段,无需json.loads service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '安装中',记录日志 logger.info(f"Install Begin -> [{service_name}]") detail_obj.install_flag = 1 detail_obj.install_msg += \ f"{self.now_time()} {service_name} 开始安装服务\n" detail_obj.save() try: # 获取服务安装脚本绝对路径 install_script_path = service_controllers_dict.get("install", "") if install_script_path == "": raise GeneralError("未找到安装脚本路径") # 获取 json 文件路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None json_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") cmd_str = f"python {install_script_path} --local_ip {target_ip} " \ f"--data_json {json_path}" # doim定制化安装 if app_name.lower() == DOIM_APP_NAME: install_script_path = os.path.realpath(install_script_path) app_dir, script_name = os.path.split(install_script_path) doim_install_script_path = os.path.join(app_dir, 'install.sh') install_dir, _service_name = os.path.split(app_dir) cmd_str = f"sed -i -e \"s#InstallRoot=.*#InstallRoot={install_dir}/DOIM #g\" -e \"s#char=.*#char=\"y\"#g\" {doim_install_script_path}; python {install_script_path} --local_ip {target_ip} " \ f"--data_json {json_path}" # 执行安装 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise GeneralError(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += \ f"{self.now_time()} 安装脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Install Failed -> [{service_name}]: {err}") detail_obj.install_flag = 3 detail_obj.install_msg += f"{self.now_time()} {service_name} " \ f"安装服务失败: {err}\n" detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 安装成功 logger.info(f"Install Success -> [{service_name}]") detail_obj.install_flag = 2 detail_obj.install_msg += \ f"{self.now_time()} {service_name} 成功安装服务\n" detail_obj.save() return True, "Install Success" def init(self, detail_obj): """ 初始化服务 """ # 获取初始化使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '初始化中',记录日志 logger.info(f"Init Begin -> [{service_name}]") detail_obj.init_flag = 1 detail_obj.init_msg += f"{self.now_time()} {service_name} 开始初始化服务\n" detail_obj.save() try: # 获取服务初始化脚本绝对路径 init_script_path = service_controllers_dict.get("init", "") if init_script_path == "": logger.info(f"Init Un Do -> [{service_name}]") detail_obj.init_flag = 2 detail_obj.init_msg += \ f"{self.now_time()} {service_name} 无需执行初始化\n" # 完成安装流程,更新状态为 '安装成功' detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS detail_obj.save() # 创建历史记录 self.create_history(detail_obj, is_success=True) return True, "Init Un Do" # 获取 json 文件路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None json_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") cmd_str = f"python {init_script_path} --local_ip {target_ip} " \ f"--data_json {json_path}" # 执行初始化 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise GeneralError(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += \ f"{self.now_time()} 初始化脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Init Failed -> [{service_name}]: {err}") detail_obj.init_flag = 3 detail_obj.init_msg += f"{self.now_time()} {service_name} " \ f"初始化服务失败: {err}\n" # 更新安装流程状态为 '失败',服务状态为 '安装失败' detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 安装成功 logger.info(f"Init Success -> [{service_name}]") detail_obj.init_flag = 2 detail_obj.init_msg += f"{self.now_time()} {service_name} 成功初始化服务\n" # 完成安装流程,更新状态为 '安装成功' # 如果是自研服务,初始化完成即认为其安装成功 if detail_obj.service.service.app_type == \ ApplicationHub.APP_TYPE_SERVICE: detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS detail_obj.save() # 创建历史记录 self.create_history(detail_obj, is_success=True) return True, "Init Success" def start(self, detail_obj): """ 启动服务 """ # 获取启动使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '启动中',记录日志 logger.info(f"Start Begin -> [{service_name}]") detail_obj.start_flag = 1 detail_obj.start_msg += f"{self.now_time()} {service_name} 开始启动服务\n" detail_obj.save() try: # 获取服务启动脚本绝对路径 start_script_path = service_controllers_dict.get("start", "") if start_script_path == "": logger.info(f"Start Un Do -> [{service_name}]") detail_obj.start_flag = 2 detail_obj.start_msg += \ f"{self.now_time()} {service_name} 无需执行启动\n" # 如果服务无需启动,则认可其为安装成功 detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS # 服务状态更新为 '正常' detail_obj.service.service_status = \ Service.SERVICE_STATUS_NORMAL detail_obj.service.save() detail_obj.save() return True, "Start Un Do" if "start" not in start_script_path: start_script_path += " start" cmd_str = f"bash {start_script_path}" # 执行启动 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout, real_timeout=self.timeout ) if not is_success: raise GeneralError(message) result_str = message.upper() if "FAILED" in result_str or \ "NO RUNNING" in result_str or \ "NOT RUNNING" in result_str: raise GeneralError(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += \ f"{self.now_time()} 启动脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Start Failed -> [{service_name}]: {err}") detail_obj.start_flag = 3 detail_obj.start_msg += f"{self.now_time()} {service_name} " \ f"启动服务失败: {err}\n" # 如果是基础组件服务的启动步骤,如果启动失败则认为其安装失败 if detail_obj.service.service.app_type == \ ApplicationHub.APP_TYPE_COMPONENT: detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() # 服务状态更新为 '停止' detail_obj.service.service_status = \ Service.SERVICE_STATUS_STOP detail_obj.service.save() return False, err # 安装成功 logger.info(f"Start Success -> [{service_name}]") detail_obj.start_flag = 2 detail_obj.start_msg += f"{self.now_time()} {service_name} 成功启动服务\n" detail_obj.save() # 服务状态更新为 '正常' detail_obj.service.service_status = \ Service.SERVICE_STATUS_NORMAL # 服务启动成功,则认为其已经安装成功 detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS detail_obj.save() detail_obj.service.save() return True, "Start Success" def execute_post_action(self, queryset, post_obj): # NOQA """ 执行安装后的操作,所有服务安装完成后,针对不同服务的个性化配置执行 目前仅支持shell脚本方式 :param queryset: 包含部署详情表的列表 :type queryset: [DetailInstallHistory] :param post_obj: :return: """ try: salt_client = SaltClient() for detail_obj in queryset: target_ip = detail_obj.service.ip _script = detail_obj.service.service_controllers.get( "post_action") command = f"chmod +x {_script.split()[0]} && {_script}" post_obj.install_log += \ f"{self.now_time()} 在{target_ip}节点执行 {command}\n" post_obj.save() flag, msg = salt_client.cmd( target=target_ip, command=command, timeout=60 ) post_obj.install_log += \ f"{self.now_time()} " \ f"在{target_ip}节点执行 {command} 标志为: {flag}; 结果为: {msg}\n" post_obj.save() logger.info(f"Execute {_script}, flag: {flag}; msg: {msg}") detail_obj.post_action_msg += str(msg) if not flag: self.is_error = True detail_obj.post_action_flag = 3 detail_obj.save() post_obj.install_flag = 3 post_obj.save() break detail_obj.post_action_flag = 2 detail_obj.save() except Exception as e: logger.error(f"Error while execute post_action: {str(e)}") self.is_error = True post_obj.install_flag = 3 post_obj.save() def single_service_executor(self, detail_obj): """ 单独服务的安装执行器 :param detail_obj: :type detail_obj: DetailInstallHistory :return: """ # 更改服务状态为安装中状态 detail_obj.service.service_status = Service.SERVICE_STATUS_INSTALLING detail_obj.service.save() # 针对单个服务执行循环("send", "unzip", "install", "init", "start") # 跳过单个服务的已经成功的单个步骤不再重复执行 for action in self.ACTION_LS: if getattr(detail_obj, f"{action}_flag") == 2: continue _flag, _msg = getattr(self, action)(detail_obj) if not _flag: return _flag, _msg return True, "success" def high_availability_executor(self, detail_obj_lst): """ 过滤专属高可用阻塞部署 :param detail_obj_lst: [detail_obj, detail_obj] :return: """ deep_detail_obj_lst = copy.deepcopy(detail_obj_lst) tmp_dict = {} for detail_obj in deep_detail_obj_lst: app_name = detail_obj.service.service.app_name if app_name in HIGH_AVAILABILITY_UTILS.keys(): tmp_dict[app_name] = tmp_dict.get(app_name, []) + [detail_obj] detail_obj_lst.remove(detail_obj) for name, obj in tmp_dict.items(): # TODO 这个需要多线程处理 HIGH_AVAILABILITY_UTILS[name](self, obj).high_thread_executor() return detail_obj_lst def thread_poll_executor(self, detail_obj_lst): """ 多线程执行器 :param detail_obj_lst: [detail_obj, detail_obj] :return: """ logger.info(f"Start thread poll executor for {detail_obj_lst}") # 阻塞部署hadoop等 detail_obj_lst = self.high_availability_executor(detail_obj_lst) with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: _future_list = [] for detail_obj in detail_obj_lst: # 更新单条安装记录的状态 detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_INSTALLING detail_obj.save() future_obj = executor.submit( self.single_service_executor, detail_obj ) _future_list.append(future_obj) for future in as_completed(_future_list): is_success, message = future.result() if not is_success: self.is_error = True break logger.info("Finish thread poll executor!") @staticmethod def make_install_order(queryset): """ 对所有的安装对象进行排序处理,控制其安装顺序 :param queryset: 即将部署的详情对象组成的列表 :return: """ # 安装顺序的二维数组 execute_lst = list() # 对基础组件进行排序处理,其中基础配置中的 BASIC_ORDER 为基础组件的排序等级 # 如果有其他组件需要安装,怎需要在配置中进行额外的配置 for i in range(10): if i not in BASIC_ORDER: break _lst = [ el for el in queryset if el.service.service.app_name in BASIC_ORDER[i] ] execute_lst.append(_lst) # 对自研服务进行排序处理,先过滤出自研服务的列表 _ser = [ el for el in queryset if el.service.service.app_type == ApplicationHub.APP_TYPE_SERVICE ] # 自研服务level级别为0的服务,仅依赖于基础组件,无其他依赖 execute_lst.append( [ el for el in _ser if str(el.service.service.extend_fields.get("level")) == "0"] ) # 自研服务level级别为1或其他的服务 # 可依赖基础组件,也可依赖其他自研服务,文件级别位置依赖 execute_lst.append( [ el for el in _ser if str(el.service.service.extend_fields.get("level")) != "0"] ) return execute_lst def execute_post_action_main(self, main_obj): """ 执行注册操作 post_action :param main_obj: :return: """ post_obj = PostInstallHistory.objects.filter( main_install_history=main_obj ).last() if not post_obj: logger.info("No need execute post action!") return True, "success" # 安装后执行动作范围过滤,排除无需执行操作以及排除已经执行成功的服务对象 # 判断整体执行安装完成后才执行 if not DetailInstallHistory.objects.filter( install_step_status=DetailInstallHistory.INSTALL_STATUS_FAILED, main_install_history_id=self.main_id ).exists() and not self.is_error: post_action_queryset = DetailInstallHistory.objects.select_related( "service", "service__service", "service__service__app_package" ).filter(main_install_history_id=self.main_id).exclude( post_action_flag__in=[2, 4] ) main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_REGISTER main_obj.save() self.execute_post_action(post_action_queryset, post_obj) if not self.is_error: post_obj.install_flag = 2 post_obj.save() return True, "success" post_obj.install_flag = 3 post_obj.save() return False, "failed" def execute_pre_install(self, main_obj): """ 执行前置安装入口 :param main_obj: :return: """ try: self.create_user_and_send_json(main_obj) except Exception as e: logger.error(f"Pre-install failed: {str(e)}") if PreInstallHistory.objects.filter( main_install_history=main_obj, install_flag__in=[0, 1, 3] ).exists(): return False return True def execute_post_install(self, main_obj): """ 执行安装后的操作,主要是针对nacos、tengine进行配置更新 :param main_obj: :return: """ post_obj = PostInstallHistory.objects.filter( main_install_history=main_obj ).last() if not post_obj: logger.info("No need execute post action!") return True, "success" if post_obj.install_flag == 2: return True, "success" post_obj.install_flag = 1 post_obj.save() # 确定重新加载的服务 tengine & nacos & aopsUtils # if DetailInstallHistory.objects.filter( # service__service__app_name__in=[ # "tengine", "nacos", "aopsUtils"] # ).exclude(main_install_history=main_obj).exists(): # for key, value in POST_INSTALL_SERVICE.items(): # if not DetailInstallHistory.objects.filter( # service__service__app_name=key).exclude( # main_install_history=main_obj # ).exists(): # continue # post_obj.install_log += \ # f"{self.now_time()} 开始执行 {key} 安装后续任务\n" # post_obj.save() # _flag, _msg = value(main_obj=main_obj).run() # post_obj.install_log += \ # f"{self.now_time()} " \ # f"{key} 安装后续任务执行标志: {_flag}; 执行结果为: {_msg}\n" # post_obj.save() # if not _flag: # post_obj.install_flag = 3 # post_obj.save() # return False, "execute post install action failed" # TODO 在增量安装的前提下,需要在加载nacos和tengine完成后,再启动其他自研服务 if self.is_error: post_obj.install_flag = 3 post_obj.save() return False, "service start failed" post_obj.install_flag = 2 post_obj.save() return True, "success" @staticmethod def get_openssl_version_from_cmd(ip, salt_client, cmd_str): """获取openssl版本号""" ssl_flag, ssl_msg = salt_client.cmd( target=ip, command=cmd_str, timeout=60 ) str_os_version = ssl_msg.strip() res = re.findall( r'(.*?) ([0-9]+)\.([0-9]+)\.([0-9]+).*', str_os_version) str_version = "".join(res[0][1:]) if res else "" if not ssl_flag: return 0 try: ssl_version = int(str_version) except ValueError: ssl_version = 0 return ssl_version @staticmethod def upgrade_openssl(ip, salt_client): # 1.发送openssl升级包 source_package_path = "openssl_upgrade/openssl-1.0.2k.tar.gz" dst_path = "/tmp/upgrade_openssl" dst_path_package = os.path.join(dst_path, "openssl-1.0.2k.tar.gz") package_flag, package_msg = salt_client.cp_file( target=ip, source_path=source_package_path, target_path=dst_path_package) if not package_flag: return False, package_msg # 2.发送openssl升级脚本 source_script_path = "openssl_upgrade/upgrade_ssl.sh" dst_script_path = os.path.join(dst_path, "upgrade_ssl.sh") script_flag, script_msg = salt_client.cp_file( target=ip, source_path=source_script_path, target_path=dst_script_path) if not script_flag: return False, script_msg # 3.执行openssl升级脚本 cmd_str = "cd /tmp/upgrade_openssl && chmod +x upgrade_ssl.sh && bash upgrade_ssl.sh" cmd_flag, cmd_msg = salt_client.cmd( target=ip, command=cmd_str, timeout=60 ) if not cmd_flag: return False, cmd_msg return True, "upgrade openssl success" def main(self): """ 主函数 """ logger.info(f"Main Install Begin, id[{self.main_id}]") # 获取主表对象,更新状态为 '安装中' main_obj = MainInstallHistory.objects.filter( id=self.main_id).first() assert main_obj is not None main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_INSTALLING main_obj.save() # 执行安装前的操作 pre_install_flag = self.execute_pre_install(main_obj=main_obj) if not pre_install_flag: main_obj.install_status = MainInstallHistory.INSTALL_STATUS_FAILED main_obj.save() return # 执行安装后的操作 _post_install_flag, _post_install_msg = self.execute_post_install( main_obj=main_obj ) if not _post_install_flag: self.is_error = True main_obj.install_status = MainInstallHistory.INSTALL_STATUS_FAILED main_obj.save() return # 获取所有安装细节表,排除已经安装成功的记录,不再重复安装 queryset = DetailInstallHistory.objects.select_related( "service", "service__service", "service__service__app_package" ).filter(main_install_history_id=self.main_id).exclude( install_step_status=DetailInstallHistory.INSTALL_STATUS_SUCCESS) # assert queryset.exists() # 构建 unzip_concurrent_controller 用于控制解压安装包时单主机的并发数量 ips = set([el.service.ip for el in queryset]) self.unzip_concurrent_controller = { el: queue.Queue(maxsize=UNZIP_CONCURRENT_ONE_HOST) for el in ips } # 所有子流程状态更新为 '待安装' queryset.update( install_step_status=DetailInstallHistory.INSTALL_STATUS_READY, ) # 将要安装的服务实例更新为 '待安装' service_ids = queryset.values_list("service_id", flat=True) Service.objects.filter( id__in=service_ids ).update(service_status=Service.SERVICE_STATUS_READY) # 对要执行安装的列表进行排序处理 tobe_execute_lst = self.make_install_order(queryset) logger.info(f"Tobe_execute_lst: {tobe_execute_lst}") for item in tobe_execute_lst: if not item: continue # 根据安装顺序每层并发执行 self.thread_poll_executor(detail_obj_lst=item) # 如果哪层的服务有安装失败的情况,那么直接退出循环 if self.is_error: break if self.is_error: # 步骤失败,主流程失败 main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_FAILED main_obj.save() # 安装完成后不再更改服务的状态 # 状态为 '待安装'/'安装中' 的记录,则记为 '失败' # queryset.filter(install_step_status__in=( # DetailInstallHistory.INSTALL_STATUS_READY, # DetailInstallHistory.INSTALL_STATUS_INSTALLING # )).update( # install_step_status=DetailInstallHistory.INSTALL_STATUS_FAILED # ) logger.info(f"Main Install Failed, id[{self.main_id}]") return self.is_error post_flag, post_msg = self.execute_post_action_main(main_obj=main_obj) if not post_flag: main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_FAILED main_obj.save() logger.error(f"Main Install Failed, id[{self.main_id}]") return self.is_error # 流程执行完整,主流程成功 main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_SUCCESS main_obj.save() # doim安装成功后,相关操作 doim_ids = list( DetailInstallHistory.objects.filter( main_install_history_id=self.main_id, service__service_instance_name__startswith=DOIM_APP_NAME ).values_list("service_id", flat=True) ) if doim_ids: service_splitting(doim_ids) logger.info(f"Main Install Success, id[{self.main_id}]") return self.is_error ================================================ FILE: omp_server/app_store/install_executor.py ================================================ """ 安装执行器 """ import os import time import logging from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from django.db.models import F from db_models.models import ( Host, Service, HostOperateLog, ServiceHistory, MainInstallHistory, DetailInstallHistory ) from utils.plugin.salt_client import SaltClient from utils.parse_config import THREAD_POOL_MAX_WORKERS logger = logging.getLogger("server") class InstallServiceExecutor: """ 安装服务执行器 """ ACTION_LS = ("send", "unzip", "install", "init", "start") def __init__(self, main_id, username, timeout=300): self.main_id = main_id self.username = username self.timeout = timeout # 安装中是否发生错误,用于流程控制 self.is_error = False @staticmethod def now_time(): return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) def create_history(self, detail_obj, is_success=True): """ 创建历史记录 """ target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name # 写入主机历史记录、服务历史记录 target_host = Host.objects.filter(ip=target_ip).first() HostOperateLog.objects.create( username=self.username, description=f"安装服务 [{service_name}]", result="success" if is_success else "failed", host=target_host) ServiceHistory.objects.create( username=self.username, description="安装服务", result="success" if is_success else "failed", service=detail_obj.service) # 主机服务数量+1 if is_success: Host.objects.filter(ip=target_ip).update( service_num=F("service_num") + 1) def send(self, detail_obj): """ 发送服务包 """ # 获取发送使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name package_name = detail_obj.service.service.app_package.package_name # 更新状态为 '发送中',记录日志 logger.info(f"Send Begin -> [{service_name}] package [{package_name}]") detail_obj.send_flag = 1 detail_obj.send_msg += f"{self.now_time()} {service_name} 开始发送服务包\n" detail_obj.save() try: # 获取目标路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None # 获取 json 文件路径 json_source_path = os.path.join( "data_files", f"{detail_obj.main_install_history.operation_uuid}.json") json_target_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") # 发送 json 文件 is_success, message = salt_client.cp_file( target=target_ip, source_path=json_source_path, target_path=json_target_path) if not is_success: raise Exception(f"发送 json 文件失败: {message}") # 获取服务包路径 source_path = os.path.join( detail_obj.service.service.app_package.package_path, package_name) target_path = os.path.join( target_host.data_folder, "omp_packages", package_name) # 发送服务包 is_success, message = salt_client.cp_file( target=target_ip, source_path=source_path, target_path=target_path) if not is_success: raise Exception(message) except Exception as err: logger.error(f"Send Failed -> [{service_name}]: {err}") detail_obj.send_flag = 3 detail_obj.send_msg += f"{self.now_time()} {service_name} " \ f"发送服务包失败: {err}\n" detail_obj.install_step_status = DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 发送成功 logger.info( f"Send Success -> [{service_name}] package [{package_name}]") detail_obj.send_flag = 2 detail_obj.send_msg += f"{self.now_time()} {service_name} 成功发送服务包\n" detail_obj.save() return True, "Send Success" def unzip(self, detail_obj): """ 解压服务包 """ # 获取解压使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name package_name = detail_obj.service.service.app_package.package_name # 更新状态为 '解压中',记录日志 logger.info( f"Unzip Begin -> [{service_name}] package [{package_name}]") detail_obj.unzip_flag = 1 detail_obj.unzip_msg += f"{self.now_time()} {service_name} 开始解压服务包\n" detail_obj.save() try: # 解析获取目录 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None package_path = os.path.join( target_host.data_folder, "omp_packages", package_name) # 获取解压目标路径 detail_args = detail_obj.install_detail_args assert detail_args is not None app_name = detail_args.get("name", None) assert app_name is not None target_path = None for info in detail_args.get("install_args", []): if info.get("key", "") == "base_dir": target_path = info.get("default") break if target_path is None: raise Exception("未获取到解压目标路径") # 切分判断路径 path_ls = os.path.split(target_path) # 创建服务目录,解压服务包 if path_ls[1] == app_name: _target_path = path_ls[0] test_path_cmd_str = f"(test -d {_target_path} || mkdir -p {_target_path}) && " \ f"tar -xmf {package_path} -C {_target_path}" else: # 当路径结尾与服务名不一致时 _target_path = path_ls[0] real_path = os.path.join(_target_path, app_name) test_path_cmd_str = f"(test -d {_target_path} || mkdir -p {_target_path}) && " \ f"tar -xmf {package_path} -C {_target_path} && mv {real_path} {target_path}/" is_success, message = salt_client.cmd( target=target_ip, command=test_path_cmd_str, timeout=self.timeout) if not is_success: raise Exception(message) except Exception as err: logger.error(f"Unzip Failed -> [{service_name}]: {err}") detail_obj.unzip_flag = 3 detail_obj.unzip_msg += f"{self.now_time()} {service_name} " \ f"解压服务包失败: {err}\n" detail_obj.install_step_status = DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 解压成功 logger.info( f"Unzip Success -> [{service_name}] package [{package_name}]") detail_obj.unzip_flag = 2 detail_obj.unzip_msg += f"{self.now_time()} {service_name} 成功解压服务包\n" detail_obj.save() return True, "Unzip Success" def install(self, detail_obj): """ 安装服务 """ # 获取安装使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name # edit by jon.liu service_controllers 为json字段,无需json.loads service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '安装中',记录日志 logger.info(f"Install Begin -> [{service_name}]") detail_obj.install_flag = 1 detail_obj.install_msg += f"{self.now_time()} {service_name} 开始安装服务\n" detail_obj.save() try: # 获取服务安装脚本绝对路径 install_script_path = service_controllers_dict.get("install", "") if install_script_path == "": raise Exception("未找到安装脚本路径") # 获取 json 文件路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None json_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") cmd_str = f"python {install_script_path} --local_ip {target_ip} --data_json {json_path}" # 执行安装 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise Exception(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += f"{self.now_time()} 安装脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Install Failed -> [{service_name}]: {err}") detail_obj.install_flag = 3 detail_obj.install_msg += f"{self.now_time()} {service_name} " \ f"安装服务失败: {err}\n" detail_obj.install_step_status = DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 安装成功 logger.info(f"Install Success -> [{service_name}]") detail_obj.install_flag = 2 detail_obj.install_msg += f"{self.now_time()} {service_name} 成功安装服务\n" detail_obj.save() return True, "Install Success" def init(self, detail_obj): """ 初始化服务 """ # 获取初始化使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '初始化中',记录日志 logger.info(f"Init Begin -> [{service_name}]") detail_obj.init_flag = 1 detail_obj.init_msg += f"{self.now_time()} {service_name} 开始初始化服务\n" detail_obj.save() try: # 获取服务初始化脚本绝对路径 init_script_path = service_controllers_dict.get("init", "") if init_script_path == "": logger.info(f"Init Un Do -> [{service_name}]") detail_obj.init_flag = 2 detail_obj.init_msg += f"{self.now_time()} {service_name} 无需执行初始化\n" # 完成安装流程,更新状态为 '安装成功' detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS detail_obj.save() # 创建历史记录 self.create_history(detail_obj, is_success=True) return True, "Init Un Do" # 获取 json 文件路径 target_host = Host.objects.filter(ip=target_ip).first() assert target_host is not None json_path = os.path.join( target_host.data_folder, "omp_packages", f"{detail_obj.main_install_history.operation_uuid}.json") cmd_str = f"python {init_script_path} --local_ip {target_ip} " \ f"--data_json {json_path}" # 执行初始化 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise Exception(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += \ f"{self.now_time()} 初始化脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Init Failed -> [{service_name}]: {err}") detail_obj.init_flag = 3 detail_obj.init_msg += f"{self.now_time()} {service_name} " \ f"初始化服务失败: {err}\n" # 更新安装流程状态为 '失败',服务状态为 '安装失败' detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_FAILED detail_obj.save() detail_obj.service.service_status = \ Service.SERVICE_STATUS_INSTALL_FAILED detail_obj.service.save() # 创建历史记录 self.create_history(detail_obj, is_success=False) return False, err # 安装成功 logger.info(f"Init Success -> [{service_name}]") detail_obj.init_flag = 2 detail_obj.init_msg += f"{self.now_time()} {service_name} 成功初始化服务\n" # 完成安装流程,更新状态为 '安装成功' detail_obj.install_step_status = \ DetailInstallHistory.INSTALL_STATUS_SUCCESS detail_obj.save() # 创建历史记录 self.create_history(detail_obj, is_success=True) return True, "Init Success" def start(self, detail_obj): """ 启动服务 """ # 获取启动使用参数 salt_client = SaltClient() target_ip = detail_obj.service.ip service_name = detail_obj.service.service_instance_name service_controllers_dict = detail_obj.service.service_controllers # 更新状态为 '启动中',记录日志 logger.info(f"Start Begin -> [{service_name}]") detail_obj.start_flag = 1 detail_obj.start_msg += f"{self.now_time()} {service_name} 开始启动服务\n" detail_obj.save() try: # 获取服务启动脚本绝对路径 start_script_path = service_controllers_dict.get("start", "") if start_script_path == "": logger.info(f"Start Un Do -> [{service_name}]") detail_obj.start_flag = 2 detail_obj.start_msg += f"{self.now_time()} {service_name} 无需执行启动\n" # 服务状态更新为 '正常' detail_obj.service.service_status = \ Service.SERVICE_STATUS_NORMAL detail_obj.service.save() detail_obj.save() return True, "Start Un Do" if "start" not in start_script_path: start_script_path += " start" cmd_str = f"bash {start_script_path}" # 执行启动 is_success, message = salt_client.cmd( target=target_ip, command=cmd_str, timeout=self.timeout) if not is_success: raise Exception(message) result_str = message.upper() if "FAILED" in result_str or \ "NO RUNNING" in result_str or \ "NOT RUNNING" in result_str: raise Exception(message) # 执行成功且 message 有值,则补充至服务日志中 if is_success and bool(message): detail_obj.install_msg += \ f"{self.now_time()} 启动脚本执行成功,脚本输出如下:\n" \ f"{message}\n" detail_obj.save() except Exception as err: logger.error(f"Start Failed -> [{service_name}]: {err}") detail_obj.start_flag = 3 detail_obj.start_msg += f"{self.now_time()} {service_name} " \ f"启动服务失败: {err}\n" detail_obj.save() # 服务状态更新为 '停止' detail_obj.service.service_status = \ Service.SERVICE_STATUS_STOP detail_obj.service.save() return False, err # 安装成功 logger.info(f"Start Success -> [{service_name}]") detail_obj.start_flag = 2 detail_obj.start_msg += f"{self.now_time()} {service_name} 成功启动服务\n" detail_obj.save() # 服务状态更新为 '正常' detail_obj.service.service_status = \ Service.SERVICE_STATUS_NORMAL detail_obj.service.save() return True, "Start Success" @staticmethod def _is_base_env(detail_obj): """ 是否为依赖项,优先执行 """ is_base_env = False try: base_env = detail_obj.service.service.extend_fields.get( "base_env", "") if isinstance(base_env, str): base_env = base_env.lower() if base_env in (True, "true"): is_base_env = True except Exception: pass return is_base_env @staticmethod def _is_dependency(detail_obj): """ 是否为依赖项,优先执行 """ return False def main(self): """ 主函数 """ logger.info(f"Main Install Begin, id[{self.main_id}]") # 获取主表对象,更新状态为 '安装中' main_obj = MainInstallHistory.objects.filter( id=self.main_id).first() assert main_obj is not None main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_INSTALLING main_obj.save() # 获取所有安装细节表 queryset = DetailInstallHistory.objects.select_related( "service", "service__service", "service__service__app_package" ).filter(main_install_history_id=self.main_id) assert queryset.exists() # 所有子流程状态更新为 '安装中' queryset.update( install_step_status=DetailInstallHistory.INSTALL_STATUS_INSTALLING) # 区分服务列表切分 base_env_ls = [] dependency_ls = [] no_dependency_ls = [] for detail_obj in queryset: if self._is_base_env(detail_obj): base_env_ls.append(detail_obj) elif self._is_dependency(detail_obj): dependency_ls.append(detail_obj) else: no_dependency_ls.append(detail_obj) logger.info(f"基础环境列表 [base_env_ls] -- {base_env_ls}") logger.info(f"含依赖列表 [dependency_ls] -- {dependency_ls}") logger.info(f"非依赖列表 [no_dependency_ls] -- {no_dependency_ls}") # TODO 含依赖项列表 -> 安装顺序排序? # 先执行 base_env 基础环境列表安装流程 with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: logger.info("Begin base_env install") for action in self.ACTION_LS: # ---- 基础环境列表并发 ---- if self.is_error: break _future_list_env = [] for detail_obj in base_env_ls: future_obj = executor.submit( getattr(self, action), detail_obj) _future_list_env.append(future_obj) for future in as_completed(_future_list_env): is_success, message = future.result() if not is_success: self.is_error = True break logger.info("End base_env install") with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: # 轮询流程列表,进行安装 logger.info("Begin else install") for action in self.ACTION_LS: logger.info(f"Enter [{action}]") # ---- 含依赖项列表轮询 ---- if self.is_error: break for detail_obj in dependency_ls: is_success, message = getattr(self, action)(detail_obj) if not is_success: self.is_error = True break # ---- 非依赖项列表并发 ---- if self.is_error: break _future_list = [] for detail_obj in no_dependency_ls: future_obj = executor.submit( getattr(self, action), detail_obj) _future_list.append(future_obj) for future in as_completed(_future_list): is_success, message = future.result() if not is_success: self.is_error = True break logger.info("End else install") if self.is_error: # 步骤失败,主流程失败 main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_FAILED main_obj.save() # 状态为 '待安装'/'安装中' 的记录,则记为 '失败' queryset.filter(install_step_status__in=( DetailInstallHistory.INSTALL_STATUS_READY, DetailInstallHistory.INSTALL_STATUS_INSTALLING )).update(install_step_status=DetailInstallHistory.INSTALL_STATUS_FAILED) logger.info(f"Main Install Failed, id[{self.main_id}]") return self.is_error # 流程执行完整,主流程成功 main_obj.install_status = \ MainInstallHistory.INSTALL_STATUS_SUCCESS main_obj.save() logger.info(f"Main Install Success, id[{self.main_id}]") return self.is_error ================================================ FILE: omp_server/app_store/install_utils.py ================================================ # -*- coding: utf-8 -*- # Project: install_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-24 14:11 # IDE: PyCharm # Version: 1.0 # Introduction: """ 安装过程中页面显示数据解析工具 """ import os import json import uuid from copy import deepcopy from concurrent.futures import ThreadPoolExecutor from django.db import transaction from omp_server.settings import PROJECT_DIR from db_models.models import ( Env, Host, ApplicationHub, ProductHub, Product, Service, ClusterInfo, ServiceConnectInfo, MainInstallHistory, DetailInstallHistory ) from app_store.tasks import install_service from utils.common.exceptions import GeneralError # from utils.plugin import public_utils from utils.plugin.salt_client import SaltClient DIR_KEY = "{data_path}" def make_lst_unique(lst, key_1, key_2): """ 去重列表内的字典 lst = [{"name": "1", "version": "1"}, {"name": "1", "version": "1"}] lst = make_lst_unique(lst, "name", "version") :param lst: 被操作对象 :param key_1: 关键key1 :param key_2: 关键key2 :return: """ unique_dic = dict() ret_lst = list() for el in lst: _unique = el.get(key_1, "") + "_" + el.get(key_2, "") if _unique in unique_dic: continue ret_lst.append(el) unique_dic[_unique] = True return ret_lst def make_editable(element): """ 处理参数是否可编辑 :param element: 参数字典 :return: """ if element.get("editable") is False or \ str(element.get("editable")).lower() == "false": element["editable"] = False else: element["editable"] = True def make_app_install_args(app_install_args): """ 构建安装参数 :param app_install_args: 服务安装参数 :type app_install_args: list :return: """ for el in app_install_args: if isinstance(el.get("default"), str) and \ DIR_KEY in el.get("default"): el["default"] = el["default"].replace(DIR_KEY, "") el["dir_key"] = DIR_KEY make_editable(el) return app_install_args class DataJson(object): """ 生成data.json数据 """ def __init__(self, operation_uuid): """ data.json数据生成方法 :param operation_uuid: 唯一操作uuid :type operation_uuid: str """ self.operation_uuid = operation_uuid def get_ser_install_args(self, obj): # NOQA """ 获取服务的安装参数 :param obj: Service :type obj: Service :return: """ deploy_detail = DetailInstallHistory.objects.get(service=obj) install_args = \ deploy_detail.install_detail_args.get("app_install_args") deploy_mode = \ deploy_detail.install_detail_args.get("deploy_mode") return { "install_args": install_args, "deploy_mode": deploy_mode } def parse_single_service(self, obj): """ 解析单个服务数据 :param obj: Service :type obj: Service :return: """ _ser_dic = { "ip": obj.ip, "name": obj.service.app_name, "instance_name": obj.service_instance_name, "cluster_name": obj.cluster.cluster_name if obj.cluster else None, "ports": json.loads(obj.service_port) if obj.service_port else [], } _others = self.get_ser_install_args(obj) _ser_dic.update(_others) return _ser_dic def make_data_json(self, json_lst): """ 创建data.json数据文件 :param json_lst: 服务及分布信息组成的列表 :type json_lst: list :return: """ _path = os.path.join( PROJECT_DIR, "package_hub/data_files", f"{self.operation_uuid}.json" ) if not os.path.exists(os.path.dirname(_path)): os.makedirs(os.path.dirname(_path)) with open(_path, "w", encoding="utf8") as fp: fp.write(json.dumps(json_lst, indent=2, ensure_ascii=False)) def run(self): """ 生成data.json方法入口 :return: """ # step1: 获取所有的服务列表 all_ser_lst = Service.objects.all() json_lst = list() for item in all_ser_lst: json_lst.append(self.parse_single_service(obj=item)) # step2: 生成data.json self.make_data_json(json_lst=json_lst) class SerDependenceParseUtils(object): """ 依赖解决工具类 """ def __init__(self, parse_name, parse_version): """ 初始化对象, 服务级别的解析,包含自研服务和基础组件服务 :param parse_name: 要解析的名称,服务 :type parse_name: str :param parse_version: 要解析的版本 :type parse_version: str """ self.parse_name = parse_name self.parse_version = parse_version self.unique_key = self.parse_name + self.parse_version def get_newest_ser(self): """ 获取最新的服务对象,同服务,同版本 :return: ApplicationHub """ return ApplicationHub.objects.filter( app_name=self.parse_name, app_version=self.parse_version, is_release=True ).last() def get_ser_instances(self, obj): # NOQA """ 查看服务是否已经被安装以及应用商店内是否具备安装条件 返回值含义: cluster_info:当前服务的集群信息 instance_info:当前服务的实例对象信息 is_pack_exist:当前服务的安装包是否存在 :param obj: ApplicationHub实例 :type obj: ApplicationHub :return: cluster_info, instance_info, is_pack_exist """ # 判断当前服务的集群 cluster_info = list(ClusterInfo.objects.filter( cluster_service_name=obj.app_name ).values("id", "cluster_name")) # 判断当前服务的实例信息 instance_info = list(Service.objects.filter( service__app_name=obj.app_name, service__app_version=obj.app_version ).values("ip", "service_instance_name", "id")) is_pack_exist = False # 判断当前应用商店内是否包含该服务以及该服务的安装包条件 if obj.app_package: path = os.path.join( PROJECT_DIR, "package_hub", obj.app_package.package_path, obj.app_package.package_name ) if os.path.exists(path): is_pack_exist = True return cluster_info, instance_info, is_pack_exist def get_deploy_mode(self, obj): # NOQA """ 解析服务的部署模式信息 [ { "key": "single", "name": "单实例" } ] :param obj: 服务对象 :type obj: ApplicationHub :return: list() """ if not obj.extend_fields or not obj.extend_fields.get("deploy", {}): return [{"key": "single", "name": "单实例"}] deploy_info = obj.extend_fields.get("deploy", {}) ret_lst = list() if "single" in deploy_info: ret_lst.extend(deploy_info["single"]) if "complex" in deploy_info: ret_lst.extend(deploy_info["complex"]) return ret_lst def get_is_base_env(self, obj): # NOQA """ 确定当前服务是否为基础环境:如 jdk 等 :param obj: 服务对象 :type obj: ApplicationHub :return: """ if not obj.extend_fields: return False return obj.extend_fields.get("base_env", False) def get_dependence(self, lst, dep): """ 解决服务依赖关系核心方法 :param lst: 存储结果的列表 :param dep: 服务依赖关系列表 :return: list() """ unique_key_lst = list() for inner in dep: _name, _version = inner.get("name"), inner.get("version") _app = ApplicationHub.objects.filter( app_name=_name, app_version=_version, is_release=True ).order_by("created").last() # 定义服务&版本唯一标准,防止递归错误 unique_key = str(_name) + str(_version) # 如果当前服务和需要被解析的源服务重叠,那么则跳过 # 如果当前服务的依赖关系已经被解决,那么则跳过 if unique_key == self.unique_key or unique_key in unique_key_lst: continue unique_key_lst.append(unique_key) # 判断当前被依赖服务是否存在,如果不存在就直接返回,不再处理深层依赖 if not _app: inner["cluster_info"] = list() inner["instance_info"] = list() inner["app_port"] = list() inner["app_install_args"] = list() inner["is_in_hub"] = False inner["is_base_env"] = False inner["is_pack_exist"] = False inner["deploy_mode"] = list() inner["process_continue"] = False inner["process_message"] = f"应用商店内未包含{_name}服务" lst.append(inner) continue # 查看当前服务是否被安装 cluster_info, instance_info, is_pack_exist = \ self.get_ser_instances(obj=_app) # 获取依赖服务的相关信息 inner["cluster_info"] = cluster_info inner["instance_info"] = instance_info # 获取依赖服务端口及其他参数信息 inner["app_port"] = \ json.loads(_app.app_port) if _app.app_port else list() if _app.app_install_args: _app_install_args = json.loads(_app.app_install_args) inner["app_install_args"] = \ make_app_install_args(_app_install_args) else: inner["app_install_args"] = list() inner["is_in_hub"] = True inner["is_base_env"] = self.get_is_base_env(obj=_app) inner["is_pack_exist"] = is_pack_exist inner["deploy_mode"] = self.get_deploy_mode(obj=_app) if cluster_info or instance_info or is_pack_exist: inner["process_continue"] = True else: inner["process_continue"] = False inner["process_message"] = f"服务{_name}未安装且无法找到安装包" lst.append(inner) if not _app.app_dependence: continue _app_dependence = json.loads(_app.app_dependence) self.get_dependence( lst, dep=_app_dependence ) def run_ser(self): """ 解析服务的依赖关系入口 :return: 服务依赖关系列表 """ _ser = self.get_newest_ser() if not _ser or not _ser.app_dependence: return list() app_dep_lst = json.loads(_ser.app_dependence) ret_lst = list() self.get_dependence(lst=ret_lst, dep=app_dep_lst) ret_lst = make_lst_unique(ret_lst, "name", "version") return ret_lst class ProDependenceParseUtils(object): """ 依赖解决工具类 """ def __init__(self, parse_name, parse_version): """ 初始化对象, 产品级别的解析 :param parse_name: 要解析的产品、应用名称 :param parse_version: 要解析的版本 """ self.parse_name = parse_name self.parse_version = parse_version self.unique_key = self.parse_name + self.parse_version def get_pro_instances(self, obj): # NOQA """ 获取产品的实例信息,被依赖产品是否已经被安装 :param obj: 应用实例对象 :type obj: ProductHub :return: """ ret_lst = Product.objects.filter( product__pro_name=obj.pro_name ).values("id", "product_instance_name") return list(ret_lst) def get_dependence(self, lst, dep): """ 解决产品依赖关系核心方法 :param lst: 存储结果的列表 :type lst: list :param dep: 服务依赖关系列表 :type dep: list :return: list() """ unique_key_lst = list() for inner in dep: _name, _version = inner.get("name"), inner.get("version") _pro = ProductHub.objects.filter( pro_name=_name, pro_version=_version, is_release=True ).order_by("created").last() # 定义服务&版本唯一标准,防止递归错误 unique_key = str(_name) + str(_version) # 如果当前产品和需要被解析的源产品重叠,那么则跳过 # 如果当前产品的依赖关系已经被解决,那么则跳过 if unique_key == self.unique_key or unique_key in unique_key_lst: continue unique_key_lst.append(unique_key) # 判断当前被依赖服务是否存在,如果不存在就直接返回,不再处理深层依赖 if not _pro: inner["instance_info"] = list() inner["is_in_hub"] = False inner["process_continue"] = False inner["process_message"] = f"应用商店内未包含{_name}应用" lst.append(inner) continue # 查看当前服务是否被安装 instance_info = self.get_pro_instances(obj=_pro) # 获取依赖服务的相关信息 inner["instance_info"] = instance_info inner["is_in_hub"] = True inner["process_continue"] = True lst.append(inner) if not _pro.pro_dependence: continue _pro_dependence = json.loads(_pro.pro_dependence) self.get_dependence( lst, dep=_pro_dependence ) def get_newest_pro(self): """ 获取最新的产品对象,同产品,同版本 :return: ProductHub """ return ProductHub.objects.filter( pro_name=self.parse_name, pro_version=self.parse_version, is_release=True ).last() def run_pro(self): """ 解析产品的依赖关系入口 :return: 产品关系依赖列表 """ _pro = self.get_newest_pro() if not _pro or not _pro.pro_dependence: return list() ret_lst = list() self.get_dependence(ret_lst, json.loads(_pro.pro_dependence)) ret_lst = make_lst_unique(ret_lst, "name", "version") return ret_lst class ServiceArgsSerializer(object): """ 服务安装过程中参数解析类 """ def get_app_dependence(self, obj): # NOQA """ 解析服务级别的依赖关系 :param obj: 服务对象 :type obj: ApplicationHub :return: """ ser = SerDependenceParseUtils(obj.app_name, obj.app_version) return ser.run_ser() def get_app_port(self, obj): # NOQA """ 获取app的端口 :param obj: 服务对象 :type obj: ApplicationHub :return: list() """ if not obj.app_port: return [] port_lst = json.loads(obj.app_port) for item in port_lst: make_editable(item) return port_lst def get_app_install_args(self, obj): # NOQA """ 解析安装参数信息 :param obj: 服务对象 :type obj: ApplicationHub :return: list() """ # 标记安装过程中涉及到的数据目录,通过此标记给前端 # 给与前端提示信息,此标记对应于主机中的数据目录 data_folder # 在后续前端提供出安装参数后,我们应该检查其准确性 if not obj.app_install_args: return list() return make_app_install_args(json.loads(obj.app_install_args)) def get_deploy_mode(self, obj): # NOQA """ 解析部署模式信息 [ { "key": "single", "name": "单实例" } ] :param obj: 服务对象 :type obj: ApplicationHub :return: """ # 如果服务未配置部署模式相关信息,那么默认为单实例模式 ser = SerDependenceParseUtils(obj.app_name, obj.app_version) return ser.get_deploy_mode(obj) def _process_continue_parse(self, obj): # NOQA """ 解析是否能够进行的核心接口 :param obj: 服务对象 :type obj: ApplicationHub :return: (bool, str) """ if not obj.app_package: return False, f"服务{obj.app_name}无安装包" # 服务级别的安装包均存在于 verified 目录内,使用 package_name 即可拼接完成 _path = os.path.join( PROJECT_DIR, "package_hub", obj.app_package.package_path, obj.app_package.package_name ) if not os.path.exists(_path): return False, f"服务{obj.app_name}的安装包无法找到" return True, "success" def get_process_continue(self, obj): # NOQA """ 解析能否继续进行的标志接口 :param obj: 服务对象 :type obj: ApplicationHub :return: bool """ flag, _ = self._process_continue_parse(obj) return flag def get_process_message(self, obj): # NOQA """ 解析能否继续进行的信息接口 :param obj: 服务对象 :type obj: ApplicationHub :return: """ _, msg = self._process_continue_parse(obj) return msg class ValidateExistService(object): """ 检查已存在服务信息是否准确 """ def __init__(self, data=None): """ 初始化方法 :param data: 要被检验的服务信息 :type data: list """ if not data or not isinstance(data, list): raise GeneralError( "ValidateExistService __init__ arg error: data") self.data = data def check_cluster(self, dic): # NOQA """ 校验集群信息是否准确 :param dic: 集群信息字典 :type dic: dict :return: """ if ClusterInfo.objects.filter(id=dic.get("id")).exists(): dic["check_flag"] = True dic["check_msg"] = "success" return dic dic["check_flag"] = False dic["check_msg"] = f"复用已存在集群{dic.get('id', 'UNKNOWN')}不存在" return dic def check_single(self, dic): # NOQA """ 检查服务的合法性 :param dic: 服务信息字典 :type dic: dict :return: """ if Service.objects.filter(id=dic.get("id")).exists(): dic["check_flag"] = True dic["check_msg"] = "success" return dic dic["check_flag"] = False dic["check_msg"] = f"复用已存在实例{dic.get('id', 'UNKNOWN')}不存在" return dic def run(self): """ 运行入口 :return: """ for item in self.data: _type = item.get("type") if _type not in ("cluster", "single"): item["check_flag"] = False item["check_msg"] = "已存在的服务信息必须在single和cluster内" continue _recheck_item = getattr(self, f"check_{_type}")(item) item.update(_recheck_item) return self.data class ValidateInstallService(object): """ 检查要安装的服务信息是否准确 """ def __init__(self, data=None): """ 初始化方法 :param data: 要被检验的服务信息 :type data: list """ if not data or not isinstance(data, list): raise GeneralError( "ValidateInstallService __init__ arg error: data" ) self.data = data def check_service_port(self, app_port, ip): # NOQA """ 检查服务端口 :param app_port: 服务端口列表 :type app_port: list :param ip: 主机ip地址 :type ip: str :return: """ salt_obj = SaltClient() for el in app_port: _port = el.get("default", "") if not _port or not str(_port).isnumeric(): el["check_flag"] = False el["check_msg"] = f"端口 {_port} 必须为数字" continue # method1: 从OMP本机查看端口是否已被占用 # _flag, _msg = public_utils.check_ip_port(ip=ip, port=int(_port)) # method2: 从目标服务器查看端口是否被占用 _flag, _msg = salt_obj.cmd( target=ip, command=f" 1 and "cluster_name" not in item) or "cluster_name" not in item: item["error_msg"] = f"{_name}应用集群实例名称[cluster_name]必须填写" is_repeat = True continue if item["cluster_name"] in cluster_name_lst: is_repeat = True item["error_msg"] = \ f"{_name}应用集群实例名称: {item['cluster_name']} 不允许重复" return is_repeat, lst def check_deploy_mode_num(self): pass def validate_data(self, data): """ 校验请求数据、返回校验结果 data: { "basic": [], "dependence": [] } :param data: :return: """ basic = data["basic"] dependence = data["dependence"] basic_repeat, basic = \ self.check_basic_product_instance_name_unique(lst=basic) dependence_repeat, dependence = \ self.check_dependence_cluster_name_unique(lst=dependence) _data = { "basic": basic, "dependence": dependence } # TODO 开源组件部署模式校验 # 使用已存在服务校验 if basic_repeat or dependence_repeat: _data["is_continue"] = False else: _data["is_continue"] = True return _data def check_service(self, validated_data, use_exist, install): # NOQA is_continue = True for item in validated_data["data"].get("basic", []): for el in item.get("services_list"): if el.get("name") not in install or \ el.get("version") != install[el.get("name")][ "version"]: el["error_msg"] = f"无法追踪此服务: {el.get('name')}" is_continue = False for item in validated_data["data"].get("use_exist", []): if item.get("is_use_exist") and \ not use_exist.get(item.get("name")): item["error_msg"] = f"此服务不存在: {item.get('name')}, 无法复用" is_continue = False return is_continue def create(self, validated_data): """ :param validated_data: :return: """ _re_obj = RedisDB() _flag, _data = _re_obj.get( name=validated_data["unique_key"] + "_step_2_origin_data" ) if not _flag: raise ValidationError(UNIQUE_KEY_ERROR) is_continue = validated_data["data"].get("is_continue") if is_continue: install = _data["install"] use_exist = _data["use_exist"] is_continue = self.check_service( validated_data=validated_data, use_exist=use_exist, install=install ) if not is_continue: validated_data["data"]["is_continue"] = False else: # 存储安装数据到redis BaseRedisData( validated_data["unique_key"] ).step_3_set_checked_data(data=validated_data) return validated_data class CreateServiceDistributionSerializer(BaseInstallSerializer): """ 生成服务分布数据 """ data = serializers.DictField( read_only=True, help_text="详细安装数据" ) def get_host_info(self): # NOQA """ 获取主机信息 :return: """ host_queryset = Host.objects.all().values( "ip", "service_num").order_by("-created") return [ {"ip": el["ip"], "num": el["service_num"]} for el in host_queryset ] def get_basic_data(self, data, all_data, check_data): # NOQA """ 获取产品应用中的服务数量信息 :param data: 产品basic列表 :type data: list :param all_data: 全部数据字典 :type all_data: dict :param check_data: 已校验过的需要安装的服务的名称及版本信息 :type check_data: dict :return: """ for item in data: services_list = item.get("services_list", []) for el in services_list: _with_ser = SerWithUtils( ser_name=el.get("name"), ser_version=check_data[el["name"]]["version"] ).run() all_data[el.get("name")] = { "num": el.get("deploy_mode"), "with": _with_ser } return all_data def get_denpendence_data(self, data, all_data, check_data): # NOQA """ 获取依赖信息的服务数量 :param data: 依赖信息列表 :type data: list :param all_data: 所有服务及数量关系字典 :type all_data: dict :param check_data: 已校验过的需要安装的服务的名称及版本信息 :type check_data: dict :return: """ for item in data: if item.get("is_use_exist") or item.get("is_base_env"): continue if isinstance(item.get("deploy_mode"), int): all_data[item.get("name")] = { "num": item.get("deploy_mode"), "with": SerWithUtils( ser_name=item.get("name"), ser_version=check_data[item["name"]]["version"] ).run() } else: # TODO 提取支持 VIP 的服务 if item.get("name") in ("mysql", "tengine"): deploy_num = \ 1 if item.get("deploy_mode") == "single" else 2 else: deploy_num = 1 all_data[item.get("name")] = { "num": deploy_num, "with": SerWithUtils( ser_name=item.get("name"), ser_version=check_data[item["name"]]["version"] ).run() } return all_data def get_product_info(self, data, lst): # NOQA """ 获取应用产品与服务的关系,为后续使用级联选择做准备 :param data: 产品信息列表 :type data: list :param lst: 返回信息列表 :type lst: list :return: """ for item in data: lst.append({ "name": item.get("name"), "child": [ el.get("name") for el in item.get("services_list", []) ] }) return lst def get_basic_info(self, data, lst): # NOQA """ :param data: 基础组件列表 :type data: list :param lst: 返回信息列表 :type lst: list :return: """ lst.append({ "name": "基础组件", "child": [ el.get("name") for el in data if not el.get("is_base_env") and not el.get("is_use_exist") ] }) return lst def create(self, validated_data): """ 校验 :param validated_data: :return: """ validated_data["data"] = dict() validated_data["data"]["host"] = self.get_host_info() _data = BaseRedisData( unique_key=validated_data["unique_key"] ).get_step_3_checked_data() check_data = BaseRedisData( unique_key=validated_data["unique_key"] ).get_step_2_origin_data() basic = _data.get("data", {}).get("basic", []) dependence = _data.get("data", {}).get("dependence", []) all_data = dict() self.get_basic_data( data=basic, all_data=all_data, check_data=check_data.get("install", {}) ) self.get_denpendence_data( data=dependence, all_data=all_data, check_data=check_data.get("install", {}) ) # 如果all_data的变量为空,那么可能安装的是base_env为True的服务,如jdk # 这些服务都应划归到基础组件级别 is_base_env_flag = False base_env_ser_name = None if not all_data: is_base_env_flag = True for key in check_data.get("install", {}).keys(): # base_env不存在集群,数量直接设置为1 base_env_ser_name = key all_data[key] = {"num": 1, "with": None} validated_data["data"]["all"] = all_data product_lst = list() self.get_product_info(data=basic, lst=product_lst) self.get_basic_info(data=dependence, lst=product_lst) if is_base_env_flag: for item in product_lst: if item.get("name") == "基础组件" and not item.get("child"): item["child"] = [base_env_ser_name] validated_data["data"]["product"] = product_lst BaseRedisData( validated_data['unique_key'] ).step_4_set_service_distribution(data=all_data) return validated_data class CheckServiceDistributionSerializer(BaseInstallSerializer): """ 检查服务分布 """ is_continue = serializers.BooleanField( help_text="可否继续下一步骤操作", read_only=True ) error_lst = serializers.ListField( help_text="错误信息列表", read_only=True ) def check_agent_status(self, ip): """ 校验主机状态 :param ip: :return: """ def validate_data(self, data): # NOQA """ 校验安装数据分布的合法性 {'10.0.14.234': ['doucApi', 'doucSso']} :param data: 服务分布字典 :type data: dict :return: """ logger.info( f"CheckServiceDistributionSerializer.validate_data: " f"data: {data}") # 校验主机及主机上的服务是否存在 ip_lst = [el["ip"] for el in Host.objects.values("ip")] error_lst = list() _salt = SaltClient() for key, value in data.items(): if key not in ip_lst: error_lst.append({"ip": key, "error_msg": f"无法找到主机{key}"}) continue _flag, _ = _salt.fun(target=key, fun="test.ping") if not _flag: error_lst.append({ "ip": key, "error_msg": f"主机 [{key}] Agent当前不在线,无法使用该主机" }) continue exist_services = Service.split_objects.filter( ip=key, service__app_name__in=value ) if exist_services.exists(): _msg = ','.join([el.service.app_name for el in exist_services]) error_lst.append( { "ip": key, "error_msg": f"主机{key}上存在重复服务: {_msg}" } ) if error_lst: data["error_lst"] = error_lst return data def create(self, validated_data): """ 校验 { 'unique_key': '886e8fc4-8e77-4de0-8123-9f3aec31ed73', 'data': { '10.0.14.234': ['doucApi', 'doucSso'] } } :param validated_data: :return: """ logger.info( f"CheckServiceDistributionSerializer.create: " f"validated_data: {validated_data}") if "error_lst" in validated_data["data"] and \ validated_data["data"]["error_lst"]: validated_data["error_lst"] = \ validated_data["data"].pop("error_lst") validated_data["is_continue"] = False return validated_data # 校验服务数量准确性 all_install_service = dict() for _, value in validated_data["data"].items(): for item in value: if item not in all_install_service: all_install_service[item] = 0 all_install_service[item] += 1 _re_obj = RedisDB() _flag, _data = _re_obj.get( name=f"{validated_data['unique_key']}_step_4_service_distribution") if not _flag: raise ValidationError(UNIQUE_KEY_ERROR) for key, value in _data.items(): if key not in all_install_service: raise ValidationError(f"缺少必须部署的服务{key}") if value["num"] != all_install_service[key]: raise ValidationError( f"服务{key}应部署{value['num']}个实例," f"实际部署{all_install_service[key]}个") # TODO 服务绑定的准确性校验 # 临时数据存储至redis BaseRedisData( validated_data['unique_key']).step_5_set_host_and_service_map( host_list=list(validated_data["data"].keys()), host_service_map=validated_data["data"] ) validated_data["is_continue"] = True validated_data["error_lst"] = list() return validated_data class CreateInstallPlanSerializer(BaseInstallSerializer): """ 创建安装计划序列化类 """ is_continue = serializers.BooleanField( help_text="可否继续下一步骤操作", read_only=True ) error_lst = serializers.ListField( help_text="错误信息列表", read_only=True ) run_user = serializers.CharField( help_text="服务运行用户", required=True, allow_null=True, allow_blank=True ) error_msg = serializers.CharField( help_text="错误信息", required=False, allow_null=True, allow_blank=True ) def check_service_dis(self, host_service_map, install_data): # NOQA """ 校验服务分布是否正确 :param host_service_map: 存储在redis中的服务与主机的对应关系 :type host_service_map: dict :param install_data: 提交上来的服务与主机的对应关系 :type install_data: dict :return: """ error_lst = list() for key, value in install_data.items(): if not host_service_map.get(key): error_lst.append({key: "此主机未被选中,请重新查看服务分布策略"}) continue if len(value) == 0: continue if len(value) != len(host_service_map.get(key)): error_lst.append({ key: f"此主机服务数量不准确," f"应选{len(host_service_map.get(key))}; " f"实际为:{len(value)}" }) continue for el in value: if el.get("name") not in host_service_map.get(key): error_lst.append({key: f"此服务{el.get('name')}不在安装范围内"}) return error_lst def make_final_install_data( # NOQA self, install_data, valid_data, run_user, host_ser_map, cluster_name_map, host_user_map ): """ 构建最终的安装数据 :param install_data: :param valid_data: :param run_user: :param host_ser_map: :param cluster_name_map: :param host_user_map: :return: """ all_install_service_lst = list() for ip, ser_lst in install_data.items(): data_folder = Host.objects.filter(ip=ip).last().data_folder if len(ser_lst) != 0: for item in ser_lst: instance_name = None for el in item["install_args"]: if el.get("key") == "instance_name": instance_name = el.get("default") item["ip"] = ip item["version"] = valid_data[item["name"]]["version"] item["install_args"] = ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=run_user, host_user_map=host_user_map ).reformat_install_args(item["install_args"]) item["data_folder"] = data_folder item["run_user"] = run_user item["instance_name"] = instance_name item["cluster_name"] = cluster_name_map.get(item["name"]) all_install_service_lst.extend(ser_lst) else: for item in host_ser_map[ip]: _dic = { "name": item, "version": valid_data[item]["version"], "ip": ip } _app = ApplicationHub.objects.filter( app_name=item, app_version=valid_data[item]["version"] ).last() _dic["data_folder"] = data_folder _dic["run_user"] = run_user _dic["install_args"] = \ ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=run_user, host_user_map=host_user_map ).remake_install_args(obj=_app) _dic["ports"] = \ ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=run_user, host_user_map=host_user_map ).get_app_port(obj=_app) _dic["instance_name"] = \ item + "-" + "-".join(ip.split(".")[-2:]) _dic["cluster_name"] = cluster_name_map.get(item) all_install_service_lst.append(_dic) return all_install_service_lst def check_error_msg(self, all_install_service_lst): # NOQA """ 检查安装参数和端口的联通性是否存在错误,数据过滤 :param all_install_service_lst: :return: """ for item in all_install_service_lst: lst = item.get("install_args", []) lst.extend(item.get("ports")) for el in lst: if "error_msg" in el and el["error_msg"]: return False, el["error_msg"] return True, "" def create(self, validated_data): """ 创建部署计划 :param validated_data: :return: """ logger.info( f"CreateInstallPlanSerializer.create: " f"validated_data: {validated_data}") run_user = validated_data["run_user"] unique_key = validated_data["unique_key"] # 添加主机与ssh连接用户的映射关系 BaseRedisData(unique_key).set_host_user_map() # 获取存储在redis中的服务与主机的映射关系 _host_ser_map = BaseRedisData( unique_key).get_step_5_host_service_map() install_data = validated_data["data"] # 查看前端发送的ip地址范围和后端存储的是否能够对应 _set_1 = set(_host_ser_map.keys()) - set(install_data.keys()) if len(_set_1) != 0: raise ValidationError( f"主机 [{','.join(list(_set_1))}] 缺失") _set_2 = set(install_data.keys()) - set(_host_ser_map.keys()) if len(_set_2) != 0: raise ValidationError( f"主机 [{','.join(list(_set_2))}] 不在已选范围内") # 校验服务数量及合法性 TODO 后期可针对服务数量准确性进行详细校验 error_lst = self.check_service_dis( host_service_map=_host_ser_map, install_data=install_data) if error_lst: validated_data["error_lst"] = error_lst validated_data["is_continue"] = False return validated_data cluster_name_map = BaseRedisData( unique_key).get_step_3_cluster_name_map() # 组装服务数据 _valid_data = BaseRedisData( unique_key).get_step_2_origin_data() _install_data = _valid_data["install"] # 获取主机与用户映射关系 host_user_map = BaseRedisData(unique_key).get_host_user_map() # _use_exist_data = _valid_data["use_exist"] all_install_service_lst = self.make_final_install_data( install_data=install_data, valid_data=_install_data, run_user=run_user, host_ser_map=_host_ser_map, cluster_name_map=cluster_name_map, host_user_map=host_user_map ) # 解决base_env服务的安装 base_env_ser_lst = BaseEnvServiceUtils( all_install_service_lst=all_install_service_lst, host_user_map=host_user_map ).run() all_install_service_lst.extend(base_env_ser_lst) # 解决带有with标识服务的解析方法 with_ser_lst = WithServiceUtils( all_install_service_lst=all_install_service_lst, unique_key=unique_key, run_user=run_user, host_user_map=host_user_map ).run() all_install_service_lst.extend(with_ser_lst) # 划分vip处理 service_vip_map = BaseRedisData( unique_key=unique_key).get_step3_service_vip_map() if service_vip_map: _keep_alive_lst = SerVipUtils( install_services=all_install_service_lst, service_vip_map=service_vip_map, host_user_map=host_user_map, run_user=run_user ).run() all_install_service_lst.extend(_keep_alive_lst) # TODO 依赖关系绑定 all_install_service_lst = ValidateInstallServicePortArgs( data=all_install_service_lst ).run() is_continue, error_msg = self.check_error_msg(all_install_service_lst) logger.info(f"Final check_error_msg: {is_continue}") logger.info(f"Final check service: {all_install_service_lst}") if not is_continue: _re_data = { el: [ ser for ser in all_install_service_lst if ser.get("ip") == el ] for el in validated_data["data"] } validated_data["data"] = _re_data validated_data["is_continue"] = is_continue validated_data["error_msg"] = error_msg return validated_data # 分配role all_install_service_lst = SerRoleUtils.get(all_install_service_lst) # 服务排序处理 all_install_service_lst = MakeServiceOrder( all_service=all_install_service_lst ).run() _flag, _res = CreateInstallPlan( all_install_service_lst=all_install_service_lst, unique_key=unique_key ).run() if not _flag: logger.error(f"Failed CreateInstallPlan: {_res}") raise _res validated_data["is_continue"] = is_continue validated_data["error_msg"] = error_msg return validated_data class MainInstallHistorySerializer(serializers.ModelSerializer): """ 历史安装记录 """ operator = serializers.CharField(max_length=32, help_text="操作用户") class Meta: """ 元数据 """ model = MainInstallHistory fields = [ "operation_uuid", "task_id", "install_status", "created", "modified", "operator" ] class CreateComponentInstallInfoSerializer(Serializer): """ 请求格式: { "high_availability": true, "install_component": [ { "name": "jenkinsNB", "version": "2.303.2" } ], "unique_key": "abf7d622-6fc8-4a04-ad4c-49b57298ecdf" } """ unique_key = serializers.CharField( help_text="操作唯一值", read_only=True ) high_availability = serializers.BooleanField( write_only=True, required=True, help_text="是否选择高可用模式", error_messages={"required": "必须包含[high_availability]字段"} ) install_component = serializers.ListField( child=serializers.DictField(), help_text="产品列表,eg: [{'name': 'ser1', 'version': '1'}]", write_only=True, required=True, error_messages={"required": "必须包含[install_component]字段"} ) data = serializers.DictField( help_text="详细安装数据", read_only=True ) def validate_install_component(self, install_component): # NOQA """ 校验即将安装的产品、应用是否在可支持范围内 :param install_component: 要安装的应用 :type install_component: list :return: """ for item in install_component: _name = item.get("name") _version = item.get("version") component_obj = ApplicationHub.objects.filter( app_name=_name, app_version=_version, is_release=True ).last() if not component_obj: raise ValidationError(f"组件 [{_name}] [{_version}] 不存在") return install_component def create(self, validated_data): """ 构建前端可选安装数据 :param validated_data: :return: """ logger.info( f"CreateComponentInstallInfoSerializer.validated_data: " f"{validated_data}") install_component = validated_data["install_component"] high_availability = validated_data["high_availability"] unique_key = str(uuid.uuid4()) _basic = list() # 遍历所有需要安装的自研服务信息,确定服务的依赖关系并存储至_dependence变量内 _dependence = list() for item in install_component: _info = ComponentServiceParse( ser_name=item["name"], ser_version=item["version"], high_availability=high_availability, unique_key=unique_key ).run() _dependence.append(_info) # 将带有with标识的服务进行处理 with_ser_lst = list() for item in install_component: _dep = SerDependenceParseUtils( parse_name=item.get("name"), parse_version=item.get("version"), high_availability=high_availability ).run_ser() _dependence.extend(_dep) if "with_flag" in item: with_ser_lst.append(item) # 将带有with标识的服务存放至redis数据中 BaseRedisData( unique_key=unique_key).step_set_with_ser(data=with_ser_lst) # 使用make_lst_unique方法将服务依赖列表去重处理 _dependence = make_lst_unique( lst=_dependence, key_1="name", key_2="version") # 判断最终用户可否进行下一步的标记 is_continue = True for item in _basic: if "error_msg" in item and item["error_msg"]: is_continue = False break for item in _dependence: if "error_msg" in item and item["error_msg"]: is_continue = False validated_data["data"] = { "basic": _basic, "dependence": _dependence, "is_continue": is_continue } # 存储基础信息到redis if is_continue: BaseRedisData( unique_key=unique_key ).step_1_set_unique_key(data=validated_data) BaseRedisData( unique_key=unique_key ).step_2_set_origin_install_data_args(data=validated_data) validated_data["unique_key"] = unique_key return validated_data class RetryInstallSerializer(Serializer): """ 重试安装序列化 """ unique_key = serializers.CharField( help_text="操作唯一值", required=True, error_messages={"required": "必须包含[unique_key]字段"} ) def validate_unique_key(self, unique_key): # NOQA """ 校验unique_key :param unique_key: :return: """ if not MainInstallHistory.objects.filter( operation_uuid=unique_key ).exists(): raise ValidationError(f"无法找到[unique_key: {unique_key}]") return unique_key def create(self, validated_data): """ :param validated_data: :return: """ unique_key = validated_data["unique_key"] main_install_history = MainInstallHistory.objects.filter( operation_uuid=unique_key ).last() # 调用异步任务,存储异步任务执行id task_id = install_service_task.delay(main_install_history.id) MainInstallHistory.objects.filter( id=main_install_history.id ).update(task_id=task_id) return validated_data ================================================ FILE: omp_server/app_store/new_install_utils.py ================================================ # -*- coding: utf-8 -*- # Project: new_install_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-11-12 14:01 # IDE: PyCharm # Version: 1.0 # Introduction: import os import json import pickle import logging import traceback from copy import deepcopy from concurrent.futures import ThreadPoolExecutor import redis from ruamel import yaml from django.db import transaction from django.db.models import F from rest_framework.exceptions import ValidationError from omp_server.settings import PROJECT_DIR from db_models.models import ( ProductHub, ApplicationHub, ClusterInfo, Service, Host, Env, ServiceConnectInfo, Product, MainInstallHistory, DetailInstallHistory, PreInstallHistory, PostInstallHistory ) from app_store.tasks import install_service as install_service_task from app_store.deploy_mode_utils import SERVICE_MAP from utils.common.exceptions import GeneralError from utils.plugin.salt_client import SaltClient from utils.parse_config import ( BASIC_ORDER, OMP_REDIS_HOST, OMP_REDIS_PORT, OMP_REDIS_PASSWORD ) from app_store.deploy_role_utils import DEPLOY_ROLE_UTILS logger = logging.getLogger("server") DIR_KEY = "{data_path}" UNIQUE_KEY_ERROR = "后台无法追踪此流程或安装流程已开始,请查看安装记录或重新进入部署流程!" WEB_CONTAINERS = ["tengine"] class RedisDB(object): """ redis数据库管理工具 """ def __init__(self): """ 获取redis连接对象 """ self.conn = redis.Redis( host=OMP_REDIS_HOST, port=OMP_REDIS_PORT, db=15, password=OMP_REDIS_PASSWORD ) def delete_keys(self, keyword): """ 删除以某个关键字开头的key :param keyword: 关键字 :return: """ for key in self.conn.scan_iter(f"{keyword}*"): self.conn.delete(key) def set(self, name, data, timeout=60 * 60 * 8): """ 设置redis键值对 :param name: redis键名称 :param data: redis中要存储的值 :param timeout: 超时时间 :return: """ self.conn.set(name, pickle.dumps(data), ex=timeout) logger.info(f"Insert data to redis:\nname:{name}\ndata:{data}") def update(self, name, data, timeout=60 * 60 * 8): """ 更新redis键值对,此时过期时间不可刷新 :param name: redis键名称 :param data: redis中要存储的值 :param timeout: 超时时间 -2是过期 -1是无过期 :return: """ _obj = self.conn.ttl(name) if _obj == -2: _obj = timeout return self.set(name, data, timeout=_obj) def get(self, name): """ 获取存储在redis中的值 :param name: redis键名称 :return: """ try: logger.info(f"Try get data from redis by name: {name}") _obj = self.conn.get(name=name) if not _obj: logger.error( f"Failed get data from redis by name: {name}, res is None") return False, None data = pickle.loads(_obj) logger.info(f"Get data from redis by name: {name}, res is {data}") return True, data except Exception as e: logger.error( f"Error while get {name} from redis: {str(e)}\n" f"{traceback.format_exc()}") return False, None class BaseRedisData(object): """ redis中存储信息配置类 """ def __init__(self, unique_key): self.unique_key = unique_key self.redis = RedisDB() def delete_all_keys(self): """ 删除unique_key相关数据 :return: """ self.redis.delete_keys(keyword=self.unique_key) def _get(self, key): """ 根据redis的key获取值 :param key: redis的key :type key: str :return: """ _flag, _data = self.redis.get(name=key) if not _flag: raise ValidationError(UNIQUE_KEY_ERROR) return _data def step_set_with_ser(self, data): """ 设置with服务范围,临时存储,在最终部署的时候添加回来 [ {"name": "xx", "version": "xxx", "with": "xxx"} ] :param data: :return: """ self.redis.set( name=self.unique_key + "_with_ser", data=data ) def get_with_ser(self): """ 获取 with ser服务的列表 :return: """ key = self.unique_key + "_with_ser" return self._get(key=key) def step_1_set_unique_key(self, data): """ 第一步 存储安装流程标志位到redis中 data数据格式为 { 'data': [{'name': 'douc', 'version': ['5.3.0']}], 'unique_key': '60649454-0149-44e8-a813-7309ffbd96ca' } :param data: 安装入口数据 :type data: dict :return: """ self.redis.set(name=self.unique_key, data=data) def get_unique_key(self): """ 获取安装流程标记 :return: """ _flag, _ = self.redis.get(name=self.unique_key) if not _flag: raise ValidationError(UNIQUE_KEY_ERROR) return self.unique_key def step_2_set_origin_install_data_args(self, data): # NOQA """ 存储要安装的数据到redis,存储的是将要安装的服务的名称,版本以及所属产品 入参data如下: { "unique_key": "60649454-0149-44e8-a813-7309ffbd96ca", "high_availability": false, "install_product": [ { "name": "douc", "version": "5.3.0" } ], "data": { "basic": [ { "name": "douc", "version": "5.3.0", "services_list": [ { "name": "doucApi", "version": "2.3.0", "deploy_mode": { "default": 1, "step": 0 } }, { "name": "doucWeb", "version": "2.3.0", "deploy_mode": { "default": 1, "step": 0 } } ], "error_msg": "" } ], "dependence": [ { "name": "kafka", "version": "2.2.2", "exist_instance": [], "is_use_exist": false, "is_base_env": false, "deploy_mode": { "default": 1, "step": 1 } }, { "name": "jdk", "version": "1.8.0", "exist_instance": [], "is_use_exist": true, "is_base_env": false, "deploy_mode": { "default": 1, "step": 1 } }, { "name": "mysql", "version": "5.7.34", "exist_instance": [], "is_use_exist": false, "is_base_env": false, "deploy_mode": [ { "key": "master-slave", "name": "主从" }, { "key": "master-master", "name": "主主(vip)" } ] } ], "is_continue": true } } 存储到redis中的数据如下: { "install": { "doucApi": { "version": "2.3.0", "product": "douc" }, "kafka": { "version": "2.2.2", "product": null } }, "use_exist": {} } :param data: 根据要安装的产品生成的安装参数 :type data: dict :return: """ basic = data.get("data", {}).get("basic") dependence = data.get("data", {}).get("dependence") _data = { "install": dict(), "use_exist": dict() } for item in basic: for el in item.get("services_list"): _data["install"][el["name"]] = { "version": el["version"], "product": item["name"] } for item in dependence: if item.get("is_use_exist"): _data["use_exist"][item["name"]] = item.get( "exist_instance", []) continue # TODO 优化版本间若依赖选择 _data["install"][item["name"]] = { # "version": item["version"], "version": ApplicationHub.objects.filter( app_name=item["name"], app_version__startswith=item["version"] ).last().app_version, "product": None } self.redis.set( name=self.unique_key + "_step_2_origin_data", data=_data ) return _data def get_step_2_origin_data(self): """ 获取安装原始数据 :return: """ key = self.unique_key + "_step_2_origin_data" return self._get(key=key) def step_3_set_checked_data(self, data): """ 存储已被校验过的安装数据 { "unique_key": "60649454-0149-44e8-a813-7309ffbd96ca", "data": { "basic": [ { "name": "douc", "version": "5.3.0", "services_list": [ { "name": "doucApi", "version": "2.3.0", "deploy_mode": 1 } ], "cluster_name": "douc-cluster-1" } ], "dependence": [ { "name": "kafka", "version": "2.2.2", "exist_instance": [], "deploy_mode": 1, "is_use_exist": false, "is_base_env": false, "cluster_name": "kafka-cluster-1" } ], "is_continue": true } } :param data: 存储点击下一步校验的数据 :type data: dict :return: """ # 覆盖第一步选择的安装数据 self.step_2_set_origin_install_data_args(data=data) self.redis.set( name=self.unique_key + "_step_3_checked_data", data=data ) cluster_name_map = dict() service_vip_map = dict() basic = data.get("data", {}).get("basic", []) for item in basic: cluster_name_map[item["name"]] = \ item.get("cluster_name") # 存储服务对应产品的集群名称 # for el in item.get("services_list"): # cluster_name_map[el["name"]] = item.get("cluster_name") dependence = data.get("data", {}).get("dependence", []) for item in dependence: if item.get("is_use_exist"): continue if item.get("vip"): service_vip_map[item.get("name")] = item.get("vip") cluster_name_map[item["name"]] = \ item.get("cluster_name") self.redis.set( name=self.unique_key + "_step_3_cluster_name_map", data=cluster_name_map ) self.redis.set( name=self.unique_key + "_step3_service_vip_map", data=service_vip_map ) def get_step_3_checked_data(self): """ 获取校验过的安装数据值 :return: """ key = self.unique_key + "_step_3_checked_data" return self._get(key=key) def get_step_3_cluster_name_map(self): """ 获取将要安装的产品的实例名称及组件的集群名称映射关系 :return: """ key = self.unique_key + "_step_3_cluster_name_map" return self._get(key=key) def get_step3_service_vip_map(self): """ 获取要是用的 vip 名 :return: """ key = self.unique_key + "_step3_service_vip_map" return self._get(key=key) def step_4_set_service_distribution(self, data): """ 存储生成的服务部署数量以及服务间的绑定关系,用于前端选择服务分布 { "doucApi": { "num": 1, "with": null }, "doucWeb": { "num": 1, "with": "portalWeb" }, "mysql": { "num": 1, "with": null }, "tengine": { "num": 1, "with": null } } :param data: 要安装的服务及数量以及服务间with绑定关系 :type data: dict :return: """ self.redis.set( name=self.unique_key + "_step_4_service_distribution", data=data ) def get_step_4_service_distribution(self): """ 获取要部署的服务的数量以及服务绑定关系 :return: """ key = self.unique_key + "_step_4_service_distribution" return self._get(key=key) def step_5_set_host_and_service_map(self, host_list, host_service_map): """ 存储本次安装所需要的主机列表及主机与服务的分布关系字典 host_list: [ "10.0.14.234", "10.0.14.231" ] host_service_map: { "10.0.14.234": [ "doucApi", "doucSso", ], "10.0.14.231": [ "portalServer", "kafka" ] } :param host_list: 本次安装涉及到的主机列表 :type host_list: list :param host_service_map: 本次安装主机与服务的映射关系 :type host_service_map: dict :return: """ self.redis.set( name=self.unique_key + "_step_5_host_list", data=host_list ) self.redis.set( name=self.unique_key + "_step_5_host_service_map", data=host_service_map ) def get_step_5_host_list(self): """ 获取本次服务部署涉及到的主机列表 :return: """ key = self.unique_key + "_step_5_host_list" return self._get(key=key) def get_step_5_host_service_map(self): """ 获取本次服务部署涉及到主机与服务的映射关系 :return: """ key = self.unique_key + "_step_5_host_service_map" return self._get(key=key) def step_6_set_final_data(self, data): """ 设置最终要安装的服务列表 :param data: :return: """ self.redis.set( name=self.unique_key + "_step_6_set_final_data", data=data ) def get_step_6_set_final_data(self): """ 获取最终要安装的服务列表 :return: """ key = self.unique_key + "_step_6_set_final_data" return self._get(key=key) def set_host_user_map(self): """ 设置主机与用户之间的关系 :return: """ host_user_lst = Host.objects.all().values("ip", "username") host_user_dic = dict() for item in host_user_lst: host_user_dic[item["ip"]] = item["username"] self.redis.set(self.unique_key + "_host_user_map", data=host_user_dic) def get_host_user_map(self): """ 获取主机与用户映射关系 :return: """ return self._get(self.unique_key + "_host_user_map") def check_package_exists(app_obj): """ 检查安装包是否存在 :param app_obj: 服务对象 :type app_obj: ApplicationHub :return: """ try: path = os.path.join( PROJECT_DIR, "package_hub", app_obj.app_package.package_path, app_obj.app_package.package_name ) if not os.path.exists(path): return False, path return True, path except Exception as e: logger.error( f"check_package_exists: Exception: {str(e)}; " f"{traceback.format_exc()}") return False, f"程序错误: {str(e)}" class ProductServiceParse(object): """ 解析要安装的应用服务的基础信息 """ def __init__( self, pro_name, pro_version, high_availability=False, unique_key=None ): """ 产品、应用解析服务信息 :param pro_name: 产品、应用名称 :param pro_version: 产品、应用版本 :param high_availability: 是否使用高可用 """ self.pro_name = pro_name self.pro_version = pro_version self.high_availability = high_availability self.unique_key = unique_key def get_default_and_step(self, app_obj): """ 获取服务默认数量及步长 :param app_obj: 服务对象 :type app_obj ApplicationHub :return: """ if not self.high_availability: return 1, 1 # 如果服务是和tengine进行强绑定的,那么需要限制其数量为1 # TODO 当前tengine不支持高可用模式 if "affinity" in app_obj.extend_fields and \ app_obj.extend_fields.get("affinity") == "tengine": return 1, 0 return 1, 1 def parse_single_service(self, service_dic): """ 解析单个服务的数据 :param service_dic: 服务名称、字典 :type service_dic: dict :return: """ _name = service_dic.get("name") _version = service_dic.get("version") app_obj = ApplicationHub.objects.filter( app_type=ApplicationHub.APP_TYPE_SERVICE, app_name=_name, app_version=_version ).last() # 解决服务强绑定依赖问题 if "affinity" in app_obj.extend_fields and \ app_obj.extend_fields["affinity"] in WEB_CONTAINERS: return { "name": _name, "version": _version, "with": app_obj.extend_fields["affinity"], "with_flag": True, "deploy_mode": { "default": 1, "step": 0 } } _default_dic = { "name": _name, "version": _version, "deploy_mode": { "default": 1, "step": 0 }, "error_msg": "" } # 如果产品下的服务不存在应该返回错误信息 if not app_obj: _default_dic["error_msg"] = \ f"应用商店内无此服务 [{_name}({_version})]" return _default_dic # 如果产品下的服务的安装包对象不存在应该返回错误信息 if not app_obj.app_package: _default_dic["error_msg"] = \ f"此服务 [{_name}({_version})] 无法找到安装包" return _default_dic # 如果安装包实际不存在应该返回错误消息 _flag, _msg = check_package_exists(app_obj=app_obj) if not _flag: _default_dic["error_msg"] = \ f"{_name}安装包不存在,请查看 {_msg}" return _default_dic # 如果是非高可用模式,直接返回服务数据 # 如果是高可用模式,那么前端服务步长为0,后端服务步长为1 default, step = self.get_default_and_step(app_obj=app_obj) return { "name": _name, "version": _version, "deploy_mode": { "default": default, "step": step } } def get_services_list(self): """ 获取产品下的服务信息列表以及其默认数量及步长 :return: """ pro_obj = ProductHub.objects.filter( pro_name=self.pro_name, pro_version=self.pro_version ).last() pro_services = json.loads(pro_obj.pro_services) return [self.parse_single_service(el) for el in pro_services] def run(self): """ 解析入口 :return: """ error_msg_lst = list() services_list = self.get_services_list() for item in services_list: if "error_msg" in item and item["error_msg"]: error_msg_lst.append(item["error_msg"]) item.pop("error_msg") _dic = { "name": self.pro_name, "version": self.pro_version, "services_list": services_list, "error_msg": "\n".join(error_msg_lst) } return _dic class ComponentServiceParse(object): """组件服务安祖昂解析""" def __init__( self, ser_name, ser_version, high_availability=False, unique_key=None ): """ 产品、应用解析服务信息 :param ser_name: 组件名称 :param ser_version: 组件版本 :param high_availability: 是否使用高可用 """ self.ser_name = ser_name self.ser_version = ser_version self.high_availability = high_availability self.unique_key = unique_key def get_deploy_mode(self, app_obj): """ 获取服务默认数量及步长 :param app_obj: 服务对象 :type app_obj ApplicationHub :return: """ pass def parse_single_service(self): """ 解析单个服务的数据 :return: """ app_obj = ApplicationHub.objects.filter( # app_type=ApplicationHub.APP_TYPE_COMPONENT, app_name=self.ser_name, app_version=self.ser_version ).last() # 解决服务强绑定依赖问题 _default_dic = { "name": self.ser_name, "version": self.ser_version, "deploy_mode": self.get_deploy_mode(app_obj=app_obj), } # 如果产品下的服务不存在应该返回错误信息 if not app_obj: _default_dic["error_msg"] = \ f"应用商店内无此服务 [{self.ser_name}({self.ser_version})]" return _default_dic _, is_pack_exist, msg = SerDependenceParseUtils( parse_name=self.ser_name, parse_version=self.ser_version, high_availability=self.high_availability ).get_ser_instances(obj=app_obj) if not is_pack_exist: _default_dic["error_msg"] = \ f"{self.ser_name}安装包不存在,请查看 {msg}" _default_dic["is_pack_exist"] = is_pack_exist _default_dic["is_use_exist"] = False _default_dic["exist_instance"] = list() _default_dic["is_base_env"] = SerDependenceParseUtils( parse_name=self.ser_name, parse_version=self.ser_version, high_availability=self.high_availability ).get_is_base_env(obj=app_obj) _default_dic["deploy_mode"] = SerDeployModeUtils( ser_name=self.ser_name, high_availability=self.high_availability ).get() return _default_dic def run(self): """ 解析入口 :return: """ return self.parse_single_service() def make_lst_unique(lst, key_1, key_2): """ 去重列表内的字典 lst = [{"name": "1", "version": "1"}, {"name": "1", "version": "1"}] lst = make_lst_unique(lst, "name", "version") :param lst: 被操作对象 :param key_1: 关键key1 :param key_2: 关键key2 :return: """ unique_dic = dict() ret_lst = list() for el in lst: _unique = el.get(key_1, "") + "_" + el.get(key_2, "") if _unique in unique_dic: continue # 服务版本模糊判断逻辑 jon.liu 20211225 _app = ApplicationHub.objects.filter( app_name=el.get(key_1), app_version__startswith=el.get(key_2) ).last() if not _app or el.get(key_1) + "_" + el.get(key_2) not in unique_dic: unique_dic[el.get(key_1) + "_" + el.get(key_2)] = True ret_lst.append(el) continue _unique = _app.app_name + "_" + _app.app_version if _unique in unique_dic: continue # 更新弱校验的服务版本 el["version"] = _app.app_version ret_lst.append(el) unique_dic[_unique] = True return ret_lst class SerDependenceParseUtils(object): """ 依赖解决工具类 """ def __init__(self, parse_name, parse_version, high_availability=False): """ 初始化对象, 服务级别的解析,包含自研服务和基础组件服务 :param parse_name: 要解析的名称,服务 :type parse_name: str :param parse_version: 要解析的版本 :type parse_version: str :param high_availability: 是否使用高可用模式 :type high_availability: bool """ self.high_availability = high_availability self.parse_name = parse_name self.parse_version = parse_version self.unique_key = self.parse_name + self.parse_version self.host_num = Host.objects.all().count() def get_newest_ser(self): """ 获取最新的服务对象,同服务,同版本 :return: ApplicationHub """ return ApplicationHub.objects.filter( app_name=self.parse_name, app_version=self.parse_version, is_release=True ).last() def get_ser_instances(self, obj): # NOQA """ 查看服务是否已经被安装以及应用商店内是否具备安装条件 返回值含义: cluster_info:当前服务的集群信息 instance_info:当前服务的实例对象信息 is_pack_exist:当前服务的安装包是否存在 :param obj: ApplicationHub实例 :type obj: ApplicationHub :return: cluster_info, instance_info, is_pack_exist """ is_pack_exist, msg = check_package_exists(app_obj=obj) if self.get_is_base_env(obj=obj): return list(), is_pack_exist, msg exist_instance = list() # 判断当前服务的集群 cluster_info = list(ClusterInfo.objects.filter( cluster_service_name=obj.app_name ).values("id", "cluster_name")) exist_instance.extend( [ { "id": el.get("id"), "name": el.get("cluster_name"), "type": "cluster" } for el in cluster_info ] ) # 判断当前服务的实例信息 instance_info = list(Service.split_objects.filter( service__app_name=obj.app_name, service__app_version=obj.app_version, cluster__isnull=True ).values("service_instance_name", "id")) exist_instance.extend( [ { "id": el.get("id"), "name": el.get("service_instance_name"), "type": "single" } for el in instance_info ] ) return exist_instance, is_pack_exist, msg def get_is_base_env(self, obj): # NOQA """ 确定当前服务是否为基础环境:如 jdk 等 :param obj: 服务对象 :type obj: ApplicationHub :return: """ if obj.is_base_env: return True if not obj.extend_fields: return False is_base_env = obj.extend_fields.get("base_env", False) if is_base_env: if isinstance(is_base_env, bool): return is_base_env if isinstance(is_base_env, str) and \ is_base_env.upper().strip() == "TRUE": return True return False def get_dependence(self, lst, dep): """ 解决服务依赖关系核心方法 :param lst: 存储结果的列表 :param dep: 服务依赖关系列表 :return: list() """ unique_key_lst = list() for inner in dep: _name, _version = inner.get("name"), inner.get("version") # 服务版本弱依赖逻辑 jon.liu 20211225 _app = ApplicationHub.objects.filter( app_name=_name, app_version__startswith=_version, is_release=True ).order_by("created").last() if _app: inner["version"] = _app.app_version _version = _app.app_version # 定义服务&版本唯一标准,防止递归错误 unique_key = str(_name) + str(_version) # 如果当前服务和需要被解析的源服务重叠,那么则跳过 # 如果当前服务的依赖关系已经被解决,那么则跳过 if unique_key == self.unique_key or unique_key in unique_key_lst: continue unique_key_lst.append(unique_key) # 判断当前被依赖服务是否存在,如果不存在就直接返回,不再处理深层依赖 if not _app: inner["exist_instance"] = list() inner["deploy_mode"] = list() inner["error_msg"] = f"应用商店内无此服务 [{_name}({_version})]" inner["is_base_env"] = False lst.append(inner) continue # 查看当前服务是否被安装 exist_instance, is_pack_exist, msg = \ self.get_ser_instances(obj=_app) # 获取依赖服务的相关信息 inner["exist_instance"] = exist_instance if inner["exist_instance"]: inner["is_use_exist"] = True else: inner["is_use_exist"] = False if not is_pack_exist: inner["error_msg"] = f"{_name}安装包不存在,请查看 {msg}" inner["is_base_env"] = self.get_is_base_env(obj=_app) # TODO 获取当前服务的部署模式 inner["deploy_mode"] = SerDeployModeUtils( ser_name=_name, high_availability=self.high_availability, host_num=self.host_num ).get() lst.append(inner) if not _app.app_dependence: continue _app_dependence = json.loads(_app.app_dependence) self.get_dependence( lst, dep=_app_dependence ) def run_ser(self): """ 解析服务的依赖关系入口 :return: 服务依赖关系列表 """ _ser = self.get_newest_ser() if not _ser or not _ser.app_dependence: return list() app_dep_lst = json.loads(_ser.app_dependence) ret_lst = list() self.get_dependence(lst=ret_lst, dep=app_dep_lst) ret_lst = make_lst_unique(ret_lst, "name", "version") return ret_lst class SerDeployModeUtils(object): """ 针对开源组件服务部署模式的获取及校验工具 """ def __init__(self, ser_name, high_availability=False, host_num=0): self.ser_name = ser_name self.high_availability = high_availability self.host_num = Host.objects.all().count() def get(self): """ 获取服务的部署模式 :return: """ if self.ser_name in SERVICE_MAP: return SERVICE_MAP[self.ser_name]( host_num=self.host_num, high_availability=self.high_availability ).get() # 如果服务不在SERVICE_MAP中,说明omp本身还未支持该服务,给出默认值 return { "default": 1, "step": 1 } class SerRoleUtils(object): """ 针对开源组件角色分配类 """ @staticmethod def get(install_services): """ 获取服务的部署模式 :return: """ tmp_dict = {} cp_service = deepcopy(install_services) for item in cp_service: if item['name'] in DEPLOY_ROLE_UTILS.keys(): tmp_dict[item['name']] = tmp_dict.get( item['name'], []) + [item] if item["name"] != "mysql": install_services.remove(item) for name, obj in tmp_dict.items(): # 发现有需要分role的实例 if name == "mysql": install_services = DEPLOY_ROLE_UTILS[name]().update_service( install_services ) else: install_services.extend( DEPLOY_ROLE_UTILS[name]().update_service(obj)) return install_services class SerVipUtils(object): """ 针对开源组件进行vip绑定处理 """ def __init__( self, install_services, service_vip_map, host_user_map, run_user): self.install_services = install_services self.service_vip_map = service_vip_map self.host_user_map = host_user_map self.run_user = run_user def get_keep_alive(self, ip, data_folder, name): _tmp_ser = dict() _app = ApplicationHub.objects.filter( app_name="keepalived", ).last() install_args = ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=self.run_user, host_user_map=self.host_user_map ).remake_install_args(obj=_app) ports = ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=self.run_user, host_user_map=self.host_user_map ).get_app_port(obj=_app) _tmp_ser["name"] = _app.app_name _tmp_ser["version"] = _app.app_version instance_name = \ "keepalived" + "-" + "-".join(ip.split(".")[-2:]) _tmp_ser["ip"] = ip _tmp_ser["data_folder"] = data_folder _tmp_ser["run_user"] = self.run_user _tmp_ser["install_args"] = install_args _tmp_ser["ports"] = ports _tmp_ser["instance_name"] = instance_name _tmp_ser["cluster_name"] = None _tmp_ser["roles"] = name return _tmp_ser def run(self): """ 处理服务的 vip 模式 :return: """ keep_alive_lst = list() for item in self.install_services: if item.get("name") in self.service_vip_map: _ser = self.get_keep_alive( ip=item.get("ip"), data_folder=item.get("data_folder"), name=item.get("name") ) _ser["vip"] = self.service_vip_map[item["name"]] keep_alive_lst.append(_ser) return keep_alive_lst class SerWithUtils(object): """ 服务强绑定关系解析类 """ def __init__(self, ser_name, ser_version): """ :param ser_name: 服务名称 :type ser_name: str """ self.ser_name = ser_name self.ser_version = ser_version def run(self): """ 获取服务绑定关系 :return: """ app_obj = ApplicationHub.objects.filter( app_name=self.ser_name, app_version=self.ser_version ).last() if not app_obj or not app_obj.extend_fields: return None if "affinity" in app_obj.extend_fields and \ app_obj.extend_fields["affinity"]: return app_obj.extend_fields["affinity"] return None class ServiceArgsPortUtils(object): """ 服务安装过程中参数解析类 """ def __init__(self, ip=None, data_folder=None, run_user=None, host_user_map=None): self.ip = ip self.data_folder = data_folder self.run_user = run_user self.host_user_map = host_user_map @staticmethod def get_product_config(): """ 获取产品配置信息,读取 config/product.yaml 配置文件 :return: """ config_path = os.path.join(PROJECT_DIR, "config/product.yaml") if not os.path.exists(config_path): return dict(), dict() with open(config_path, "r") as fp: product_config = yaml.load(fp, Loader=yaml.SafeLoader) return product_config.get("install", dict()), \ product_config.get("ports", dict()) @staticmethod def inner_replace_args(target_lst, config_lst): """ 更新配置参数方法 :param target_lst: 源配置列表 :param config_lst: 需要更新的配置列表 :return: """ try: config_dic = {el["key"]: el for el in config_lst} new_lst = list() for item in target_lst: if item.get("key") in config_dic: new_lst.append(config_dic[item.get("key")]) else: new_lst.append(item) return new_lst except Exception as e: raise GeneralError( f"更新安装参数错误, " f"请检查 {os.path.join(PROJECT_DIR, 'config/product.yaml')} " f"数据格式是否符合规则, 错误信息: {str(e)}" ) def make_product_config_overwrite(self, app_name, rep_type, lst): """ 覆盖原有数据库中的yaml,利用 config/product.yaml 中的配置文件覆盖相关参数 :param app_name: 服务名称 :param rep_type: 替换类型, app_install_args | app_ports :param lst: 替换参数列表 :return: """ app_install_args_dic, app_ports_dic = self.get_product_config() if rep_type == "app_install_args" and app_name in app_install_args_dic: return self.inner_replace_args( target_lst=lst, config_lst=app_install_args_dic[app_name] ) elif rep_type == "app_ports" and app_name in app_ports_dic: return self.inner_replace_args( target_lst=lst, config_lst=app_ports_dic[app_name] ) return lst @staticmethod def make_editable(element): """ 处理参数是否可编辑 :param element: 参数字典 :return: """ if element.get("editable") is False or \ str(element.get("editable")).lower() == "false": element["editable"] = False else: element["editable"] = True def get_app_dependence(self, obj): # NOQA """ 解析服务级别的依赖关系 :param obj: 服务对象 :type obj: ApplicationHub :return: """ ser = SerDependenceParseUtils(obj.app_name, obj.app_version) return ser.run_ser() def get_app_port(self, obj): # NOQA """ 获取app的端口 :param obj: 服务对象 :type obj: ApplicationHub :return: list() """ if not obj.app_port: return [] origin_ports = json.loads(obj.app_port) final_ports = self.make_product_config_overwrite( app_name=obj.app_name, rep_type="app_ports", lst=origin_ports ) for item in final_ports: self.make_editable(item) return final_ports def format_app_install_args(self, app_install_args): # NOQA """ 构建安装参数 :param app_install_args: 服务安装参数 :type app_install_args: list :return: """ for el in app_install_args: if isinstance(el.get("default"), str) and \ DIR_KEY in el.get("default"): el["default"] = el["default"].replace(DIR_KEY, "") el["dir_key"] = DIR_KEY return app_install_args def get_app_install_args(self, obj): # NOQA """ 解析安装参数信息 :param obj: 服务对象 :type obj: ApplicationHub :return: list() """ # 标记安装过程中涉及到的数据目录,通过此标记给前端 # 给与前端提示信息,此标记对应于主机中的数据目录 data_folder # 在后续前端提供出安装参数后,我们应该检查其准确性 if not obj.app_install_args: return list() origin_args = json.loads(obj.app_install_args) final_args = self.make_product_config_overwrite( app_name=obj.app_name, rep_type="app_install_args", lst=origin_args ) for item in final_args: self.make_editable(item) return self.format_app_install_args(final_args) def reformat_install_args(self, install_args): """ 重新格式化前端已经修改过了的install_args参数 :param install_args: 前端修改过的安装参数 :type install_args: list :return: """ return self._parse(app_install_args=install_args) def remake_install_args(self, obj): """ 重新生成部署时需要使用的参数,相当于直接从数据库中解析完整的安装参数 :param obj: ApplicationHub对象 :type obj: ApplicationHub :return: """ if not obj.app_install_args: return list() app_install_args = self.make_product_config_overwrite( app_name=obj.app_name, rep_type="app_install_args", lst=json.loads(obj.app_install_args) ) return self._parse(app_install_args) def _parse(self, app_install_args): """ 格式化解析安装参数数据 :param app_install_args: 安装参数 :type app_install_args: list :return: """ for el in app_install_args: if DIR_KEY in el.get("default") or "dir_key" in el: el["default"] = os.path.join( self.data_folder, el["default"].replace(DIR_KEY, "").lstrip("/") ) el["dir_key"] = DIR_KEY if el.get("key") == "run_user": if self.host_user_map[self.ip] != "root": el["default"] = self.host_user_map[self.ip] continue if self.run_user: el["default"] = self.run_user return app_install_args class ValidateInstallService(object): """ 检查要安装的服务信息是否准确 """ def __init__(self, data=None): """ 初始化方法 :param data: 要被检验的服务信息 :type data: list """ if not data or not isinstance(data, list): raise GeneralError( "ValidateInstallService __init__ arg error: data" ) self.data = data def check_service_port(self, app_port, ip): # NOQA """ 检查服务端口 :param app_port: 服务端口列表 :type app_port: list :param ip: 主机ip地址 :type ip: str :return: """ salt_obj = SaltClient() for el in app_port: _port = el.get("default", "") if not _port or not str(_port).isnumeric(): el["check_flag"] = False el["error_msg"] = f"端口 {_port} 必须为数字" continue # method1: 从OMP本机查看端口是否已被占用 # _flag, _msg = public_utils.check_ip_port(ip=ip, port=int(_port)) # method2: 从目标服务器查看端口是否被占用 _flag, _msg = salt_obj.cmd( target=ip, command=f" 0: continue if item["ip"] in base_env_dic and \ el["name"] in base_env_dic[item["ip"]]: continue _ins_name = \ el["name"] + "-" + "-".join(item["ip"].split(".")[-2:]) base_env_ser_lst.append({ "name": el["name"], "version": el["version"], "ip": item["ip"], "install_args": ServiceArgsPortUtils( ip=item["ip"], data_folder=item["data_folder"], run_user=item["run_user"], host_user_map=self.host_user_map ).remake_install_args(obj=_dep_obj), "ports": ServiceArgsPortUtils( ip=item["ip"], data_folder=item["data_folder"], run_user=item["run_user"], host_user_map=self.host_user_map ).get_app_port(obj=_dep_obj), "instance_name": _ins_name }) if item["ip"] not in base_env_dic: base_env_dic[item["ip"]] = list() base_env_dic[item["ip"]].append(el["name"]) def run(self): """ 解析base_env数据入口 :return: """ base_env_dic = dict() base_env_ser_lst = list() for item in self.all: app_obj = ApplicationHub.objects.filter( app_name=item["name"], app_version=item["version"] ).last() # 当前服务没有依赖的情况 if not app_obj.app_dependence or \ not json.loads(app_obj.app_dependence): continue _dep_lst = json.loads(app_obj.app_dependence) self._dep_parse( dep_lst=_dep_lst, base_env_dic=base_env_dic, base_env_ser_lst=base_env_ser_lst, item=item ) return base_env_ser_lst class WithServiceUtils(object): """ 解决带有with的服务场景 """ def __init__(self, all_install_service_lst, unique_key=None, run_user=None, host_user_map=None): """ 解决依赖于base_env服务的安装操作 :param all_install_service_lst: 所有需要安装的服务 :type all_install_service_lst: list :param unique_key: 安装标识 :type unique_key: str :param run_user: 运行用户 :type run_user: str :param host_user_map: 主机与运行用户关系 :type host_user_map: str """ self.all = all_install_service_lst self.unique_key = unique_key self.run_user = run_user self.host_user_map = host_user_map def parse_single_service(self, ser_dic): """ 解析单个服务的with场景,并拼接为最终可安装的数据格式 :param ser_dic: 服务信息 :type ser_dic: dict :return: """ _ser = { "name": ser_dic.get("name"), "version": ser_dic.get("version") } _ser_lst = list() with_ser = ser_dic.get("with") is_found_flag = False # 是否已经找到with服务的标识 for item in self.all: _tmp_ser = deepcopy(_ser) # 当需要被with的服务在需要安装的服务列表中时 if item.get("name") == with_ser: ip = item.get("ip") data_folder = item.get("data_folder") run_user = item.get("run_user") _app = ApplicationHub.objects.filter( app_name=_tmp_ser["name"], app_version=_tmp_ser["version"] ).last() install_args = ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=run_user, host_user_map=self.host_user_map ).remake_install_args(obj=_app) ports = ServiceArgsPortUtils( ip=ip, data_folder=data_folder, run_user=run_user, host_user_map=self.host_user_map ).get_app_port(obj=_app) instance_name = \ _tmp_ser["name"] + "-" + "-".join(ip.split(".")[-2:]) _tmp_ser["ip"] = ip _tmp_ser["data_folder"] = data_folder _tmp_ser["run_user"] = run_user _tmp_ser["install_args"] = install_args _tmp_ser["ports"] = ports _tmp_ser["instance_name"] = instance_name _tmp_ser["cluster_name"] = None _ser_lst.append(_tmp_ser) is_found_flag = True # 如果没有找到,则证明被with的服务是在复用的列表内的,需要查看当前系统内的该服务信息 if not is_found_flag: with_ser_ips = Service.split_objects.filter( service__app_name=with_ser).values("ip") with_ser_ips = [el["ip"] for el in with_ser_ips] for _ip in with_ser_ips: _tmp_ser = deepcopy(_ser) data_folder = Host.objects.filter(ip=_ip).last().data_folder run_user = self.run_user _app = ApplicationHub.objects.filter( app_name=_tmp_ser["name"], app_version=_tmp_ser["version"] ).last() install_args = ServiceArgsPortUtils( ip=_ip, data_folder=data_folder, run_user=run_user, host_user_map=self.host_user_map ).remake_install_args(obj=_app) ports = ServiceArgsPortUtils( ip=_ip, data_folder=data_folder, run_user=run_user, host_user_map=self.host_user_map ).get_app_port(obj=_app) instance_name = \ _tmp_ser["name"] + "-" + "-".join(_ip.split(".")[-2:]) _tmp_ser["ip"] = _ip _tmp_ser["data_folder"] = data_folder _tmp_ser["run_user"] = run_user _tmp_ser["install_args"] = install_args _tmp_ser["ports"] = ports _tmp_ser["instance_name"] = instance_name _tmp_ser["cluster_name"] = None _ser_lst.append(_tmp_ser) return _ser_lst def run(self): """ 运行方法 :return: """ with_ser_lst = BaseRedisData( unique_key=self.unique_key).get_with_ser() ret_lst = list() for item in with_ser_lst: ret_lst.extend(self.parse_single_service(ser_dic=item)) return ret_lst class DataJson(object): """ 生成data.json数据 """ def __init__(self, operation_uuid, service_obj=None): """ data.json数据生成方法 :param operation_uuid: 唯一操作uuid :param service_obj: service 操作对象,可为空 :type operation_uuid: str """ self.operation_uuid = operation_uuid self.service_obj = service_obj def get_ser_install_args(self, obj): # NOQA """ 获取服务的安装参数 :param obj: Service :type obj: Service :return: """ deploy_detail = DetailInstallHistory.objects.get(service=obj) install_args = \ deploy_detail.install_detail_args.get("install_args") deploy_mode = \ deploy_detail.install_detail_args.get("deploy_mode") return { "install_args": install_args, "deploy_mode": deploy_mode } def parse_single_service(self, obj): """ 解析单个服务数据 :param obj: Service :type obj: Service :return: """ _ser_dic = { "ip": obj.ip, "name": obj.service.app_name, "role": obj.service_role if obj.service_role else "master", "instance_name": obj.service_instance_name, "cluster_name": obj.cluster.cluster_name if obj.cluster else None, "ports": json.loads(obj.service_port) if obj.service_port else [], "dependence": json.loads(obj.service_dependence) if obj.service_dependence else [], "vip": obj.vip } _others = self.get_ser_install_args(obj) _ser_dic.update(_others) return _ser_dic def make_data_json(self, json_lst): """ 创建data.json数据文件 :param json_lst: 服务及分布信息组成的列表 :type json_lst: list :return: """ _path = os.path.join( PROJECT_DIR, "package_hub/data_files", f"{self.operation_uuid}.json" ) if not os.path.exists(os.path.dirname(_path)): os.makedirs(os.path.dirname(_path)) with open(_path, "w", encoding="utf8") as fp: fp.write(json.dumps(json_lst, indent=2, ensure_ascii=False)) def run(self): """ 生成data.json方法入口 :return: """ # step1: 获取所有的服务列表 # edit by vum: # 服务中表中会有残留 base_env 服务的情况,类似 jdk,此时不宜获取所有服务 if self.service_obj: all_ser_lst = self.service_obj else: all_ser_lst = Service.split_objects.all() json_lst = list() for item in all_ser_lst: json_lst.append(self.parse_single_service(obj=item)) # 在json文件中标记该服务所在主机上的agent的地址 ip_agent_dir_dir = { el["ip"]: el["agent_dir"] for el in Host.objects.values("ip", "agent_dir") } for item in json_lst: item["agent_dir"] = ip_agent_dir_dir.get(item.get("ip")) # step2: 生成data.json self.make_data_json(json_lst=json_lst) class CreateInstallPlan(object): """ 生成部署计划相关数据 """ def __init__(self, all_install_service_lst, unique_key=None): """ :param all_install_service_lst: """ logger.info(f"CreateInstallPlan.__init__: {all_install_service_lst}") self.install_services = all_install_service_lst self.unique_key = unique_key def get_app_obj_for_service(self, dic): # NOQA """ 获取服务实例表中关联的app对象 :param dic: 服务数据 :type dic: dict :return: """ return ApplicationHub.objects.filter( app_name=dic["name"], app_version__startswith=dic["version"] ).last() def get_controllers_for_service(self, dic): # NOQA """ 获取服务控制脚本信息 :param dic: 服务数据 :type dic: dict :return: """ # 获取关联application对象 _app = self.get_app_obj_for_service(dic) # TODO 确定application表内的app_controllers字段存储类型 _app_controllers = json.loads(_app.app_controllers) # 获取服务家目录 install_args = dic["install_args"] _home = "" data_folder = Host.objects.filter(ip=dic["ip"]).last().data_folder for el in install_args: if "dir_key" in el and el["key"] == "base_dir": _home = el["default"] real_home = os.path.join(data_folder, _home.rstrip("/")) _new_controller = dict() # 更改服务控制脚本、拼接相对路径 for key, value in _app_controllers.items(): if not value: continue _new_controller[key] = os.path.join(real_home, value) # 如果该服务需要在整体安装完成后有一些操作,那么需要重新构建post_action # 在每次安装完所有服务后,需要搜索出相应的post_action并统一执行 if "post_action" in _app.extend_fields and \ _app.extend_fields["post_action"]: _new_controller["post_action"] = os.path.join( real_home, _app.extend_fields["post_action"] ) return _new_controller def get_env_for_service(self): # NOQA """ 获取当前环境 :return: """ # TODO 暂时使用默认环境 return Env.objects.last() def create_connect_info(self, dic): # NOQA """ 创建或获取服务的用户名、密码信息 :param dic: 服务数据 :type dic: dict :return: """ username = password = username_enc = password_enc = "" for item in dic["install_args"]: if not item["default"]: continue if item["key"] == "username": username = item["default"] if item["key"] == "password": password = item["default"] if item["key"] == "username_enc": username_enc = item["default"] if item["key"] == "password_enc": password_enc = item["default"] if username or password or username_enc or password_enc: _ser_conn_obj, _ = ServiceConnectInfo.objects.get_or_create( service_name=dic["name"], service_username=username, service_password=password, service_username_enc=username_enc, service_password_enc=password_enc ) return _ser_conn_obj return None def create_cluster(self, dic): # NOQA """ 创建集群信息 :param dic: 服务数据 :type dic: dict :return: """ if "cluster_name" not in dic or not dic["cluster_name"]: return None _app_obj = self.get_app_obj_for_service(dic) # 根据要安装的服务是组件还是应用,这里仅做组件级别的集群 if _app_obj.app_type != 0: return None # 如果存在则获取、如果不存在则创建 cluster_obj, _ = ClusterInfo.objects.get_or_create( cluster_service_name=dic["name"], cluster_name=dic["cluster_name"], service_connect_info=self.create_connect_info(dic) ) return cluster_obj def _get_exist_dep(self, inner): """ 获取已存在依赖信息 :param inner: :type inner: dict :return: """ if inner.get("type") == "cluster": cl_obj = ClusterInfo.objects.filter(id=inner["id"]).last() return { "name": cl_obj.cluster_service_name, "cluster_name": cl_obj.cluster_name, "instance_name": None } ser_obj = Service.split_objects.filter(id=inner["id"]).last() return { "name": ser_obj.service.app_name, "cluster_name": None, "instance_name": ser_obj.service_instance_name } def _get_install_dep(self, inner): """ 获取将要安装服务中的被依赖项 :param inner: :return: """ _ser_name = inner["name"] _ser_version = inner["version"] for el in self.install_services: if el.get("name") == _ser_name and \ el.get("version").startswith(_ser_version): cluster_name = el.get("cluster_name") instance_name = el.get("instance_name") if cluster_name: instance_name = None else: cluster_name = None return { "name": el.get("name"), "cluster_name": cluster_name, "instance_name": instance_name } raise ValidationError( f"无法找到被依赖的服务[{_ser_name}({_ser_version})]") def get_dependence(self, dic): """ 获取服务依赖的实例信息 :param dic: :return: """ _obj = self.get_app_obj_for_service(dic) _dep_lst = list() if not _obj.app_dependence or not json.loads(_obj.app_dependence): return [] lst = json.loads(_obj.app_dependence) exist_data = BaseRedisData( unique_key=self.unique_key ).get_step_2_origin_data().get("use_exist") for item in lst: # 已存在的base_env服务依赖 _dep_obj = ApplicationHub.objects.filter( app_name=item.get("name"), app_version__startswith=item.get("version") ).last() if _dep_obj.is_base_env: _ser_obj = Service.split_objects.filter( service=_dep_obj, ip=dic.get("ip") ).last() if _ser_obj: _dep_lst.append({ "name": item.get("name"), "cluster_name": None, "instance_name": _ser_obj.service_instance_name }) continue if item.get("name") in exist_data: _de = exist_data[item["name"]] _dep_lst.append(self._get_exist_dep(_de)) else: _dep_lst.append(self._get_install_dep(item)) return _dep_lst def create_service(self, dic): """ 创建服务实例 :param dic: 服务数据 :type dic: dict :return: """ # 创建服务实例对象,默认从安装来的服务的状态为 安装中 _ser_obj = Service( ip=dic["ip"], service_instance_name=dic["instance_name"], service=self.get_app_obj_for_service(dic), service_port=json.dumps(dic.get("ports")), service_role=dic.get("roles", ""), service_controllers=self.get_controllers_for_service(dic), cluster=self.create_cluster(dic), env=self.get_env_for_service(), service_status=6, service_connect_info=self.create_connect_info(dic), service_dependence=json.dumps(self.get_dependence(dic)), vip=dic.get("vip") ) _ser_obj.save() return _ser_obj def create_product_instance(self, dic): # NOQA """ 创建产品实例 :param dic: 服务数据 :type dic: dict :return: """ _obj = self.get_app_obj_for_service(dic) if not _obj or _obj.app_type != ApplicationHub.APP_TYPE_SERVICE: return _data = BaseRedisData( unique_key=self.unique_key).get_step_3_checked_data() for item in _data.get("data", {}).get("basic", []): if _obj.product.pro_name == item.get("name") and \ _obj.product.pro_version == item.get("version"): product_instance_name = item.get("cluster_name") Product.objects.get_or_create( product_instance_name=product_instance_name, product=_obj.product ) def check_if_has_post_action(self, ser): # NOQA """ 检测是否需要执行安装后的动作 :param ser: 服务对象 :type ser Service :return: """ if "post_action" in ser.service_controllers and \ ser.service_controllers.get("post_action"): return True return False def create_pre_install_history(self, main_obj): """ 创建安装计划,在执行安装计划时,优先执行此处的操作记录 :param main_obj: :return: """ _data = list(DetailInstallHistory.objects.filter( main_install_history=main_obj ).values_list("service__ip", flat=True)) for item in set(_data): PreInstallHistory( main_install_history=main_obj, ip=item ).save() def create_post_install_history(self, main_obj): """ 创建安装后的行为规范 :param main_obj: :return: """ # 在安装完成后,需要执行一些操作 # 在这里进行定制化处理,所有的安装操作都需要执行下重新加载nacos和tengine的配置 # 此处专为 云智慧 进行定制 PostInstallHistory(main_install_history=main_obj).save() # post_action_queryset = DetailInstallHistory.objects.select_related( # "service", "service__service", "service__service__app_package" # ).filter(main_install_history=main_obj).exclude( # post_action_flag__in=[2, 4] # ) # if post_action_queryset.exists(): # PostInstallHistory(main_install_history=main_obj).save() # return # logger.info(f"Do execute_post_install for {main_obj.operation_uuid}") # if DetailInstallHistory.objects.filter( # main_install_history=main_obj, # service__service__app_type=ApplicationHub.APP_TYPE_SERVICE # ).exists(): # PostInstallHistory(main_install_history=main_obj).save() def run(self): """ 服务部署信息入库操作 :return: """ try: logger.info("start CreateInstallPlan.run!") with transaction.atomic(): # step0: 生成 # step1: 生成操作唯一uuid,创建主安装记录 operation_uuid = self.unique_key main_obj = MainInstallHistory( operation_uuid=operation_uuid, install_status=0, install_args=self.install_services ) main_obj.save() # step2: 创建安装细节表 for item in self.install_services: # 创建服务实例对象 ser_obj = self.create_service(item) # 创建产品实例对象 self.create_product_instance(item) post_action_flag = 0 if self.check_if_has_post_action( ser_obj) else 4 DetailInstallHistory( service=ser_obj, main_install_history=main_obj, install_detail_args=item, post_action_flag=post_action_flag ).save() # 创建安装前的操作记录 self.create_pre_install_history(main_obj) # 创建安装后操作日志 self.create_post_install_history(main_obj) _json_obj = DataJson(operation_uuid=operation_uuid) _json_obj.run() # 调用安装异步任务,并回写异步任务到 task_id = install_service_task.delay(main_obj.id) MainInstallHistory.objects.filter(id=main_obj.id).update( task_id=task_id ) # 删除redis中缓存的安装临时数据 BaseRedisData(unique_key=self.unique_key).delete_all_keys() # 更新主机上的服务数量 _ser_ip_lst = DetailInstallHistory.objects.filter( main_install_history=main_obj, service__service__is_base_env=False ).values("service__ip") _tmp_dic = dict() for item in _ser_ip_lst: if item["service__ip"] not in _tmp_dic: _tmp_dic[item["service__ip"]] = 0 _tmp_dic[item["service__ip"]] += 1 for key, value in _tmp_dic.items(): Host.objects.filter(ip=key).update( service_num=F("service_num") + value) except Exception as e: logger.error( f"CreateInstallPlan.run failed with error: \n" f"{traceback.format_exc()}") return False, e return True, operation_uuid class MakeServiceOrder(object): def __init__(self, all_service): self.all_service = all_service def run(self): return self.make_install_order() def make_install_order(self): """ :return: """ final_lst = list() # 对基础组件进行排序处理,其中基础配置中的 BASIC_ORDER 为基础组件的排序等级 # 如果有其他组件需要安装,怎需要在配置中进行额外的配置 for i in range(10): if i not in BASIC_ORDER: break _lst = [ el for el in self.all_service if el.get("name") in BASIC_ORDER[i] ] final_lst.extend(_lst) all_self_app = ApplicationHub.objects.filter( app_type=ApplicationHub.APP_TYPE_SERVICE ).values("app_name", "app_version", "extend_fields") all_ser_dic = { el["app_name"] + "_" + el["app_version"]: el for el in all_self_app } level_0_lst = list() level_1_lst = list() for item in self.all_service: _key = item["name"] + "_" + item["version"] if _key not in all_ser_dic: continue if str(all_ser_dic[_key]["extend_fields"].get("level")) == "0": level_0_lst.append(item) else: level_1_lst.append(item) final_lst.extend(level_0_lst) final_lst.extend(level_1_lst) return final_lst class ValidateInstallServicePortArgs(object): """ 检查要安装的服务信息是否准确 """ def __init__(self, data=None): """ 初始化方法 :param data: 要被检验的服务信息 :type data: list """ if not data or not isinstance(data, list): raise GeneralError( "ValidateInstallService __init__ arg error: data" ) self.data = data def check_service_port(self, app_port, ip): # NOQA """ 检查服务端口 :param app_port: 服务端口列表 :type app_port: list :param ip: 主机ip地址 :type ip: str :return: """ salt_obj = SaltClient() for el in app_port: _port = el.get("default", "") if not _port or not str(_port).isnumeric(): el["check_flag"] = False el["error_msg"] = f"端口 {_port} 必须为数字" continue # method1: 从OMP本机查看端口是否已被占用 # _flag, _msg = public_utils.check_ip_port(ip=ip, port=int(_port)) # method2: 从目标服务器查看端口是否被占用 _flag, _msg = salt_obj.cmd( target=ip, command=f" 200: self.db_obj.update_package_status( 1, f"yml校验description长度过长,检查yml文件{self.yaml_dir}") return False db_filed['description'] = description return True, db_filed def service_component(self, settings): """校验kind为service""" # service骨架弱校验 db_filed = {} # post_action affinity base_env auto_launch不再校验 # level 默认0 monitor不做校验 first_check = {"ports", "install", "control"} if not self.check_obj.weak_check(settings, first_check, attention=str(first_check)): return False # auto_launch 校验 不填写默认给true settings["auto_launch"] = settings.get("auto_launch", "true") # base_env 校验 不填写True true 全部按照false处理 db_filed['base_env'] = settings.pop('base_env', "") # ports 校验 ports = settings.pop('ports') ports_strong_check = {"name", "protocol", "default", "key"} port = self.check_obj.strong_check( ports, ports_strong_check, is_weak=True, ignore={"key"}, attention="ports") if ports else 1 if not port: return False db_filed['ports'] = ports # control校验 control = settings.pop('control') control_weak_check = {"start", "stop", "restart", "install", "init"} control_check = self.check_obj.weak_check( control, control_weak_check, attention=str(control_weak_check)) if control else 1 if not control_check: return False control_strong_check = self.check_obj.strong_check( control, {"install"}, attention="control") if not control_strong_check: return False db_filed['control'] = control # install 校验 install = settings.pop('install') single_strong_install = {"name", "key", "default"} install_check = self.check_obj.strong_check( install, single_strong_install, is_weak=True, attention="install") if install else 1 if not install_check: return False db_filed['install'] = install # monitor 校验 db_filed['monitor'] = settings.pop('monitor', None) return True, db_filed def service(self, settings): """ 创建服务校验类,原服务类变为基类 """ level = settings.pop('level', -1) if level == -1: level = 0 result = self.service_component(settings) if isinstance(result, bool): return False settings['level'] = level return True, result[1] def upgrade(self, settings): return self.service_component(settings) def component(self, settings): # 校验label,继承service db_filed = {} label = settings.pop('labels', None) if not label: self.db_obj.update_package_status( 1, f"yml校验labels失败,检查yml文件{self.yaml_dir}") return False db_filed['labels'] = label description = settings.pop('description', "-1") if description == "-1": self.db_obj.update_package_status( 1, f"yml校验description校验失败,检查yml文件{self.yaml_dir}") return False if description is not None and len(description) > 200: self.db_obj.update_package_status( 1, f"yml校验description长度过长,检查yml文件{self.yaml_dir}") return False db_filed['description'] = description result = self.service_component(settings) if isinstance(result, bool): return False db_filed.update(result[1]) return True, db_filed def exec_clear(clear_dir): # 清理逻辑,当发布完成时对临时路径进行清空处理, # 此逻辑会考虑状态为正在校验和正在发布状态的情况 # 存在清理跳过,后期会考虑只选择一段时间内状态非中间态不做清理逻辑。, online = UploadPackageHistory.objects.filter( is_deleted=False, package_status__in=[2, 5]).count() if len(clear_dir) <= 28: logger.error(f'{clear_dir}路径异常') return None if online == 0 or 'back_end_verified' in clear_dir: clear_out = public_utils.local_cmd( f'rm -rf {clear_dir}') logger.info(clear_dir) if clear_out[2] != 0: logger.error('清理环境失败') def clear_check(need_rm): """ 梳理要删除的包 """ result = [] for tmp_dir in need_rm: if ".tar" in tmp_dir[0]: result.append(tmp_dir[0]) result.append(tmp_dir[2].rsplit('/', 1)[0]) else: rm_dir = tmp_dir[0].rsplit('/', 1)[0] result.append(rm_dir) result.append(f"{rm_dir[:-10]}*") logger.info("需要删除的目录:" + " ".join(result)) return " ".join(result) @shared_task def publish_bak_end(uuid, exc_len): """ 后台扫描同步等待发布函数 params: uuid 当前唯一操作id exc_len 合法安装包个数 """ # 增加try,并增加超时机制释放锁 exc_task = True time_count = 0 try: while exc_task and time_count <= 600: # 当所有安装包的状态均不为正在校验, # 并和扫描出得包的个数相同且不为0,进行发布逻辑。 valid_uuids = UploadPackageHistory.objects.filter( operation_uuid=uuid, package_parent__isnull=True, ).exclude( package_status=2) valid_success = valid_uuids.exclude( package_status=1).count() if valid_uuids.count() != exc_len: time_count += 1 time.sleep(5) else: if valid_uuids.count() != 0 and valid_success != 0: publish_entry(uuid) else: exec_clear("{0}/*".format(os.path.join( package_hub, package_dir.get('back_end_verified')))) exc_task = False finally: re = redis.Redis(host=OMP_REDIS_HOST, port=OMP_REDIS_PORT, db=9, password=OMP_REDIS_PASSWORD) re.delete('back_end_verified') @shared_task def publish_entry(uuid): """ 前台发扫描台发布函数公共类 params: uuid 当前唯一操作id 注:此异步任务的调用的前提必须是校验已完成状态 """ # 修改校验无误的安装包的状态为正在发布状态。 valid_uuids = UploadPackageHistory.objects.filter( is_deleted=False, operation_uuid=uuid, package_parent__isnull=True, package_status=0) valid_uuids.update(package_status=5) valid_uuids = UploadPackageHistory.objects.filter( is_deleted=False, operation_uuid=uuid, package_parent__isnull=True, package_status=5) valid_packages = {} # 从库获取校验合法的安装包名称 if valid_uuids: for j in valid_uuids: valid_packages[j.package_name] = j json_data = os.path.join(project_dir, 'data', f'middle_data-{uuid}.json') with open(json_data, "r", encoding="utf8") as fp: lines = fp.readlines() valid_info = [] # 将匹配到的安装包和中间结果的安装包比对,将安装包名替换成历史记录表的model对象 for line in lines: json_line = json.loads(line) valid_obj = valid_packages.get(json_line.get('package_name')) if json_line.get("extend_fields").get("product"): valid_info.append(json_line) # 升级包单独逻辑 elif valid_obj: json_line['package_name'] = valid_obj valid_info.append(json_line) valid_packages_obj = [] valid_dir = None # 中间结果转入创表函数 product_obj = None tmp_dir = [] front_dir = [] for line in valid_info: if line.get('kind') == 'product': CreateDatabase(line).create_product() elif line.get('kind') == 'service': product = line.get("extend_fields").get("product") product_obj = ProductHub.objects.filter(pro_name=product.get("name"), pro_version=product.get( "version") ).last() upload_history_obj = None for j in valid_uuids: if j.package_name == line.get('package_name'): upload_history_obj = j # 更新服务的upload历史状态和已有服务关联 upload_history_obj.package_parent = product_obj.pro_package upload_history_obj.save() CreateDatabase(line).create_service([line], product_obj) line['package_name'] = upload_history_obj else: CreateDatabase(line).create_component() tmp_dir = line.get('tmp_dir') if len(tmp_dir[0]) <= 28: line['package_name'].package_status = 4 line['package_name'].save() logger.error(f'{tmp_dir[0]}路径异常') return None # 匹配到的安装包移动至对应路径 # 产品包路径 package_hub/xxx/random/product_name/xxx # 组件路径 package_hub/xxx/app.tar.gz # 产品目标路径 verified/product_name-version/xxx # 组件目标路径 verified/app.tar.gz valid_name = tmp_dir[0].rsplit('/', 1) valid_pk = f"{valid_name[1]}-{tmp_dir[1]}" if len( tmp_dir) == 2 else valid_name[1] if product_obj and line.get('kind') == 'service': valid_pk = f"{product_obj.pro_name}-{product_obj.pro_version}/{valid_name[1]}" valid_dir = os.path.join(project_dir, 'package_hub', 'verified', valid_pk) move_tmp = "/".join(valid_name) move_out = public_utils.local_cmd( f'rm -rf {valid_dir} && mv {move_tmp} {valid_dir}') if move_out[2] != 0: line['package_name'].package_status = 4 line['package_name'].save() logger.error('移动或删除失败') valid_packages_obj.append(line['package_name'].id) if "front_end_verified" in tmp_dir[0]: front_dir.append(tmp_dir) clear_dir = os.path.dirname(tmp_dir[0]) if os.path.isfile(valid_dir) else \ os.path.dirname(os.path.dirname(tmp_dir[0])) UploadPackageHistory.objects.filter(id__in=valid_packages_obj).update( package_status=3) need_rm = clear_check(front_dir) if not need_rm: need_rm = f"{clear_dir}/*" exec_clear(need_rm) def check_monitor_data(detail_obj): """ 根据部署详情信息,确认该服务的要监控的方式,并返回监控处理结果 { "type": "JavaSpringBoot", "metric_port": "{service_port}", "process_name": "" } :param detail_obj: 部署详情对象 :type detail_obj: DetailInstallHistory :return: (bool, _ret_dic) """ def get_port(keyword): """ 根据关键字获取端口值 :param keyword: :return: """ service_port_ls = json.loads(detail_obj.service.service_port) for item in service_port_ls: if item.get("key") == keyword: return item.get("default") return None _ret_dic = { "listen_port": get_port("service_port"), "metric_port": None, "run_port": [], "only_process": False, "process_key_word": None, "type": None } run_port_key_list = list() run_port_value_list = list() app_monitor = detail_obj.service.service.app_monitor if not app_monitor: return False, _ret_dic run_port_key_list = app_monitor.get("run_port", []) if len(run_port_key_list) > 0: run_port_value_list = [get_port(key) for key in run_port_key_list] if None not in run_port_value_list: _ret_dic["run_port"] = run_port_value_list _ret_dic["type"] = app_monitor.get("type") _ret_dic["process_key_word"] = app_monitor.get("process_name") if isinstance(app_monitor.get("metric_port"), str): _metric_port_key = app_monitor.get( "metric_port", "").replace("{", "").replace("}", "") elif isinstance(app_monitor.get("metric_port"), dict): _metric_port_key = list(app_monitor.get("metric_port", {}).keys())[0] else: _metric_port_key = None if detail_obj.service.service_port is not None: _ret_dic["metric_port"] = get_port(_metric_port_key) if _ret_dic["metric_port"]: return True, _ret_dic if _ret_dic["process_key_word"]: _ret_dic["only_process"] = True return True, _ret_dic return False, _ret_dic def add_prometheus(main_history_id, queryset=None): """ 添加服务到 Prometheus """ logger.info("Add Prometheus Begin") prometheus = PrometheusUtils() # TODO 不同类型服务添加监控方式不同,后续版本优化 # 仅更新已经安装完成的最新服务 # 给monitor_agent刷新使用,提供detail_obj list。 queryset = queryset if queryset else DetailInstallHistory.objects.filter( main_install_history_id=main_history_id, install_step_status=DetailInstallHistory.INSTALL_STATUS_SUCCESS ) for detail_obj in queryset: try: _flag, _monitor_dic = check_monitor_data(detail_obj=detail_obj) logger.info( f"Add Prometheus get monitor_dic for " f"{detail_obj}: {_monitor_dic}") except Exception as e: logger.info( f"Add Prometheus get monitor_dic Failed: " f"{detail_obj}; {str(e)}") continue if not _flag: continue # TODO 已是否具有端口作为是否需要添加监控的依据,后续版本优化 instance_name = detail_obj.service.service_instance_name service_port = _monitor_dic.get("listen_port") run_port = _monitor_dic.get("run_port", []) # 获取数据目录、日志目录 app_install_args = detail_obj.install_detail_args.get( "install_args", []) data_dir = log_dir = "" username = password = "" for info in app_install_args: if info.get("key", "") == "data_dir": data_dir = info.get("default", "") if info.get("key", "") == "log_dir": log_dir = info.get("default", "") if info.get("key", "") == "username": username = info.get("default", "") if info.get("key", "") == "password": password = info.get("default", "") # TODO 后期优化 ser_name = detail_obj.service.service.app_name if ser_name == "hadoop": ser_name = instance_name.split("_", 1)[0] # 添加服务到 prometheus is_success, message = prometheus.add_service({ "service_name": ser_name, "instance_name": instance_name, "data_path": data_dir, "log_path": log_dir, "env": "default", "ip": detail_obj.service.ip, "listen_port": service_port, "run_port": run_port, "metric_port": _monitor_dic.get("metric_port"), "only_process": _monitor_dic.get("only_process"), "process_key_word": _monitor_dic.get("process_key_word"), "username": username, "password": password, }) if not is_success: logger.error( f"Add Prometheus Failed {instance_name}, error: {message}") continue logger.info(f"Add Prometheus Success {instance_name}") logger.info("Add Prometheus End") def make_inspection(username): """ 触发巡检任务 :param username: :return: """ logger.info("安装成功后触发巡检任务") from rest_framework.test import APIClient from rest_framework.reverse import reverse from db_models.models import UserProfile from db_models.models import Env data = { "inspection_name": "mock", "inspection_type": "deep", "inspection_status": "1", "execute_type": "man", "inspection_operator": username, "env": Env.objects.last().id } user = UserProfile.objects.filter(username=username).last() client = APIClient() client.force_authenticate(user) res = client.post( path=reverse("history-list"), data=json.dumps(data), content_type="application/json" ).json() logger.info(f"安装成功后触发巡检任务的结果为: {res}") return res @shared_task def install_service(main_history_id, username="admin"): """ 安装服务 :param main_history_id: MainInstallHistory 主表 id :param username: 执行用户 :return: """ try: # 为防止批量安装时数据库写入数据过多,这里采用循环的方式判断main_history_id try_times = 0 while try_times < 3: if MainInstallHistory.objects.filter(id=main_history_id).exists(): break time.sleep(5) else: logger.error( "Install Service Task Failed: can not find {main_history_id}") executor = InstallServiceExecutor(main_history_id, username) executor.main() logger.error(f"Install Service Task Success [{main_history_id}]") except Exception as err: import traceback logger.error(f"Install Service Task Failed [{main_history_id}], " f"err: {err}") logger.error(traceback.format_exc()) # 更新主表记录为失败 MainInstallHistory.objects.filter( id=main_history_id).update( install_status=MainInstallHistory.INSTALL_STATUS_FAILED) # 安装成功,则注册服务至监控 add_prometheus(main_history_id) # 安装成功,触发巡检任务 if MainInstallHistory.objects.filter( id=main_history_id, install_status=MainInstallHistory.INSTALL_STATUS_SUCCESS ).exists(): make_inspection(username=username) ================================================ FILE: omp_server/app_store/tmp_exec_back_task.py ================================================ import os from utils.parse_config import ( OMP_REDIS_PORT, OMP_REDIS_PASSWORD, OMP_REDIS_HOST ) import time from app_store.tasks import front_end_verified, publish_bak_end import redis from db_models.models import UploadPackageHistory import random current_dir = os.path.dirname(os.path.abspath(__file__)) project_dir = os.path.dirname(os.path.dirname(current_dir)) package_hub = os.path.join(project_dir, "package_hub") package_dir = { "back_end_verified": "back_end_verified", "front_end_verified": "front_end_verified", "verified": "verified" } class RedisLock(object): def __init__(self, host='127.0.0.1', port='6379', db=9, **kwargs): self.rdcon = self.args_init(host, port, db, kwargs) @staticmethod def args_init(host, port, db, kwargs): if kwargs and kwargs.get('password'): return redis.Redis(host, port, db, kwargs['password']) return redis.Redis(host, port, db) def get_lock(self, key='back_end_verified'): if self.rdcon.exists(key) == 0: return False, self.rdcon return True, self.rdcon def back_end_verified_init(operation_user): """ 后台扫描接口 :param operation_user: 操作用户 :return: """ # uuid 自己生成,上redis锁,如果存在则返回当前锁及包名 uuid = str(round(time.time() * 1000)) redis_obj = RedisLock( host=OMP_REDIS_HOST, port=OMP_REDIS_PORT, password=OMP_REDIS_PASSWORD) lock, redis_key = redis_obj.get_lock() if lock: return redis_key.lindex("back_end_verified", 1).decode("utf-8"), redis_key. \ lindex("back_end_verified", 0).decode("utf-8").split(",") back_verified = os.path.join( package_hub, package_dir.get('back_end_verified')) service_name = os.listdir(back_verified) exec_name = [ p for p in service_name if os.path.isfile(os.path.join(back_verified, p)) and (p.endswith('.tar') or p.endswith('.tar.gz')) ] redis_key.lpush("back_end_verified", uuid, ",".join(exec_name)) # 设置过期时间,同时创建异步校验任务及发布任务 redis_key.expire("back_end_verified", 3600) for j in exec_name: upload_obj = UploadPackageHistory( operation_uuid=uuid, operation_user=operation_user, package_name=j, package_md5='1', package_path="verified") upload_obj.save() front_end_verified_init(uuid, operation_user, j, upload_obj.id) publish_bak_end.delay(uuid, len(exec_name)) return uuid, exec_name def front_end_verified_init(uuid, operation_user, package_name, obj_id, md5=None): # 前端发布校验接口 random_str = ''.join( random.sample('abcdefghijklmnopqrstuvwxyz1234567890', 10)) if md5: ver_dir = package_dir.get("front_end_verified") else: ver_dir = package_dir.get("back_end_verified") front_end_verified.delay(uuid, operation_user, package_name, random_str, ver_dir, obj_id) ================================================ FILE: omp_server/app_store/upload_task.py ================================================ import os from db_models.models import ApplicationHub, ProductHub, UploadPackageHistory, Labels import logging from celery.utils.log import get_task_logger import json logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(os.path.dirname(CURRENT_DIR)) class CreateDatabase(object): """ 创建产品表,服务表,标签表公共类 params json_data创建表所需要的json label_type json归属类型 eg: 1 产品类型 """ def __init__(self, json_data): self.json_data = json_data self.label_type = None def str_to_bool(self, value): """ str转换bool值 """ str_bool = self.json_data.get(value) result = "true" if str_bool.lower() == "true" else None return bool(result) def explain(self, data, default=None): """ 将dict list转换成 json """ data_info = self.json_data.get(data) if data_info: if isinstance(data_info, dict) or isinstance(data_info, list): data_info = json.dumps(data_info, ensure_ascii=False) else: data_info = default return data_info def explain_dependence(self): # 不符合常规逻辑,慎用 data_info = self.json_data.get("dependencies") if isinstance(data_info, list): for key in data_info: if isinstance(key, dict): key["version"] = key.get("version", "").split(".")[0] data_info = json.dumps(data_info, ensure_ascii=False) return data_info def create_product(self): """ 创建产品表 """ self.label_type = 1 self.create_lab() _dic = { "is_release": True, "pro_name": self.json_data.get('name'), "pro_version": self.json_data.get('version'), "pro_description": self.json_data.get('description'), "pro_dependence": self.explain('dependencies'), "pro_services": self.explain('service', []), "pro_package": self.json_data.get('package_name'), "pro_logo": self.json_data.get('image'), "extend_fields": self.json_data.get("extend_fields") } # app_obj = ProductHub( # is_release=True, pro_name=self.json_data.get('name'), # pro_version=self.json_data.get('version'), # pro_description=self.json_data.get('description'), # pro_dependence=self.explain('dependencies'), # pro_services=self.explain('service'), # pro_package=self.json_data.get('package_name'), # pro_logo=self.json_data.get('image'), # extend_fields=self.json_data.get("extend_fields") # ) pro_queryset = ProductHub.objects.filter( pro_name=self.json_data.get('name'), pro_version=self.json_data.get('version') ) if pro_queryset.exists(): pro_queryset.update(**_dic) app_obj = pro_queryset.first() else: app_obj = ProductHub.objects.create(**_dic) app_obj.save() # 创建lab表 self.create_pro_app_lab(app_obj) service = self.json_data.pop('product_service') # 创建服务表 self.create_service(service, app_obj) def create_service(self, service, app_obj): """ 创建服务表 params service service的json字段,格式同json_data一致 app_obj 需要关联产品表的对象 """ pro_history = app_obj.pro_package service_obj = UploadPackageHistory.objects.filter( package_parent=pro_history) valid_packages = {} for j in service_obj: valid_packages[j.package_name] = j valid_info = [] for line in service: valid_obj = valid_packages.get(line.get('package_name')) if valid_obj: line['package_name'] = valid_obj valid_info.append(line) # 实例化变量添加label标签,创建产品名称标签,类型组件 self.json_data['labels'] = [app_obj.pro_name] self.label_type = 0 self.create_lab() pro_services = json.loads(app_obj.pro_services) name_ls = [name.get('name') for name in pro_services] version_ls = [version.get('version') for version in pro_services] for info in valid_info: self.json_data = info # 服务json添加labels字段,给create_pro_app_lab做筛选 self.json_data['labels'] = [app_obj.pro_name] # 按照服务名和版本进行划分 如果存在则覆盖,否则创建 monitor = self.json_data.get( "monitor") if self.json_data.get("monitor") else None _dic = { "is_release": True, "app_type": 1, "app_name": self.json_data.get("name", ""), "app_version": self.json_data.get("version", ""), "app_port": self.explain("ports", json.dumps([])), "app_dependence": self.explain_dependence(), "app_install_args": self.explain("install"), "app_controllers": self.explain("control"), "app_package": self.json_data.get("package_name"), "product": app_obj, "extend_fields": self.json_data.get("extend_fields"), "is_base_env": self.str_to_bool("base_env"), "app_monitor": monitor } app_queryset = ApplicationHub.objects.filter( app_name=self.json_data.get("name"), app_version=self.json_data.get("version") ) if _dic["app_name"] not in name_ls \ or _dic["app_version"] not in version_ls: pro_services.append({"name": _dic["app_name"], "version": _dic["app_version"]}) if app_queryset.exists(): app_queryset.update(**_dic) else: ser_obj = ApplicationHub.objects.create(**_dic) # 做多对多关联 self.create_pro_app_lab(ser_obj) app_obj.pro_services = json.dumps(pro_services, ensure_ascii=False) app_obj.save() # ApplicationHub.objects.create( # is_release=True, app_type=1, # app_name=self.json_data.get("name"), # app_version=self.json_data.get("version"), # app_description=self.json_data.get("description"), # app_port=self.explain("ports"), # app_dependence=self.explain("dependencies"), # app_install_args=self.explain("install"), # app_controllers=self.explain("control"), # app_package=self.json_data.get("package_name"), # product=app_obj, # extend_fields=self.json_data.get("extend_fields") # ) def create_component(self): """ 创建组件表 逻辑同创建产品表一致 """ self.label_type = 0 self.create_lab() monitor = self.json_data.get( "monitor") if self.json_data.get("monitor") else None _dic = { "is_release": True, "app_type": 0, "app_name": self.json_data.get("name"), "app_version": self.json_data.get("version"), "app_description": self.json_data.get("description"), "app_port": self.explain("ports"), "app_dependence": self.explain_dependence(), "app_install_args": self.explain("install"), "app_controllers": self.explain("control"), "app_package": self.json_data.get("package_name"), "extend_fields": self.json_data.get("extend_fields"), "app_logo": self.json_data.get("image"), "is_base_env": self.str_to_bool("base_env"), "app_monitor": monitor } app_queryset = ApplicationHub.objects.filter( app_name=self.json_data.get("name"), app_version=self.json_data.get("version") ) if app_queryset.exists(): app_queryset.update(**_dic) app_obj = app_queryset.first() else: app_obj = ApplicationHub.objects.create(**_dic) # app_obj = ApplicationHub.objects.create( # is_release=True, app_type=0, # app_name=self.json_data.get("name"), # app_version=self.json_data.get("version"), # app_description=self.json_data.get("description"), # app_port=self.explain("ports"), # app_dependence=self.explain("dependencies"), # app_install_args=self.explain("install"), # app_controllers=self.explain("control"), # app_package=self.json_data.get("package_name"), # extend_fields=self.json_data.get("extend_fields"), # app_logo=self.json_data.get("image") # ) # app_obj.save() self.create_pro_app_lab(app_obj) def create_pro_app_lab(self, obj): """ 创建lab表和服务表应用表做多对多关联 """ labels = self.json_data.get('labels') for i in labels: label_obj = Labels.objects.get( label_name=i, label_type=self.label_type) if self.label_type == 1: obj.pro_labels.add(label_obj) else: obj.app_labels.add(label_obj) def create_lab(self): """ 创建lab表,未存在的名称做创建,已存在的跳过处理 """ labels_obj = Labels.objects.filter(label_type=self.label_type) labels = [i.label_name for i in labels_obj] compare_labels = set(self.json_data.get('labels')) - set(labels) compare_list = [] if compare_labels: for compare_label in compare_labels: label_obj = Labels(label_name=compare_label, label_type=self.label_type) compare_list.append(label_obj) Labels.objects.bulk_create(compare_list) ================================================ FILE: omp_server/app_store/urls.py ================================================ # -*- coding: utf-8 -*- # Project: urls # Author: jon.liu@yunzhihui.com # Create time: 2021-10-08 15:54 # IDE: PyCharm # Version: 1.0 # Introduction: from rest_framework.routers import DefaultRouter from app_store.views import ( LabelListView, ComponentListView, ServiceListView, UploadPackageView, RemovePackageView, ComponentDetailView, ServiceDetailView, ServicePackPageVerificationView, PublishViewSet, ExecuteLocalPackageScanView, LocalPackageScanResultView, ApplicationTemplateView, DeploymentPlanValidateView, DeploymentPlanImportView, DeploymentPlanListView, DeploymentOperableView, DeploymentTemplateView, ExecutionRecordAPIView, DeleteAppStorePackageView, ProductCompositionView) from app_store.views_for_install import ( ComponentEntranceView, ProductEntranceView, ExecuteInstallView, InstallHistoryView, ServiceInstallHistoryDetailView ) from app_store.new_install_view import ( BatchInstallEntranceView, CreateInstallInfoView, CheckInstallInfoView, CreateServiceDistributionView, CheckServiceDistributionView, GetInstallHostRangeView, GetInstallArgsByIpView, CreateInstallPlanView, ListServiceByIpView, ShowInstallProcessView, ShowSingleServiceInstallLogView, MainInstallHistoryView, CreateComponentInstallInfoView, RetryInstallView ) router = DefaultRouter() router.register("labels", LabelListView, basename="labels") router.register("components", ComponentListView, basename="components") router.register("services", ServiceListView, basename="appServices") router.register("upload", UploadPackageView, basename="upload") router.register("remove", RemovePackageView, basename="remove") router.register("delete", DeleteAppStorePackageView, basename="delete") router.register("componentDetail", ComponentDetailView, basename="componentDetail") router.register("serviceDetail", ServiceDetailView, basename="appServiceDetail") router.register("pack_verification_results", ServicePackPageVerificationView, basename="pack_verification_results") router.register("publish", PublishViewSet, basename="publish") router.register("executeLocalPackageScan", ExecuteLocalPackageScanView, basename="executeLocalPackageScan") router.register("localPackageScanResult", LocalPackageScanResultView, basename="localPackageScanResult") router.register( "executeLocalPackageScan", ExecuteLocalPackageScanView, basename='executeLocalPackageScan') router.register( "localPackageScanResult", LocalPackageScanResultView, basename='localPackageScanResult') router.register( "applicationTemplate", ApplicationTemplateView, basename='applicationTemplate') router.register( "deploymentPlanValidate", DeploymentPlanValidateView, basename="deploymentPlanValidate" ) router.register( "deploymentPlanImport", DeploymentPlanImportView, basename="deploymentPlanImport" ) router.register( "deploymentPlanList", DeploymentPlanListView, basename="deploymentPlanList" ) router.register( "deploymentOperable", DeploymentOperableView, basename="deploymentOperable" ) router.register( "deploymentTemplate", DeploymentTemplateView, basename="deploymentTemplate" ) # 安装部分使用路由 router.register( "componentEntrance", ComponentEntranceView, basename='componentEntrance') router.register( "productEntrance", ProductEntranceView, basename='productEntrance') router.register( "executeInstall", ExecuteInstallView, basename='executeInstall') router.register( "installHistory", InstallHistoryView, basename='installHistory') router.register( "serviceInstallHistoryDetail", ServiceInstallHistoryDetailView, basename='serviceInstallHistoryDetail') # 新版安装逻辑 router.register( "batchInstallEntrance", BatchInstallEntranceView, basename="batchInstallEntrance" ) router.register( "createInstallInfo", CreateInstallInfoView, basename="createInstallInfo" ) router.register( "checkInstallInfo", CheckInstallInfoView, basename="checkInstallInfo" ) router.register( "createServiceDistribution", CreateServiceDistributionView, basename="createServiceDistribution" ) router.register( "checkServiceDistribution", CheckServiceDistributionView, basename="checkServiceDistribution" ) router.register( "getInstallHostRange", GetInstallHostRangeView, basename="getInstallHostRange" ) router.register( "getInstallArgsByIp", GetInstallArgsByIpView, basename="getInstallArgsByIp" ) router.register( "createInstallPlan", CreateInstallPlanView, basename="createInstallPlan" ) router.register( "listServiceByIp", ListServiceByIpView, basename="listServiceByIp" ) router.register( "showInstallProcess", ShowInstallProcessView, basename="showInstallProcess" ) router.register( "showSingleServiceInstallLog", ShowSingleServiceInstallLogView, basename="showSingleServiceInstallLog" ) router.register( "mainInstallHistory", MainInstallHistoryView, basename="mainInstallHistory" ) router.register( "createComponentInstallInfo", CreateComponentInstallInfoView, basename="createComponentInstallInfo" ) router.register( "retryInstall", RetryInstallView, basename="retryInstall" ) router.register( "executionRecord", ExecutionRecordAPIView, basename="ExecutionRecord" ) # 产品类型接口查看及修改 router.register( "productComposition", ProductCompositionView, basename="productComposition" ) ================================================ FILE: omp_server/app_store/views.py ================================================ """ 应用商店相关视图 """ import os import uuid import json import time import string import random import logging from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ( ListModelMixin, CreateModelMixin ) from rest_framework.response import Response from rest_framework.exceptions import ValidationError from django.db import transaction from django_filters.rest_framework.backends import DjangoFilterBackend from db_models.models import ( Labels, ApplicationHub, Product, ProductHub, Env, Host, Service, ServiceConnectInfo, DeploymentPlan, ClusterInfo, MainInstallHistory, DetailInstallHistory, PreInstallHistory, PostInstallHistory, UploadPackageHistory, ExecutionRecord) from utils.common.paginations import PageNumberPager from app_store.app_store_filters import ( LabelFilter, ComponentFilter, ServiceFilter, UploadPackageHistoryFilter, PublishPackageHistoryFilter ) from app_store.app_store_serializers import ( ComponentListSerializer, ServiceListSerializer, UploadPackageSerializer, RemovePackageSerializer, UploadPackageHistorySerializer, ExecuteLocalPackageScanSerializer, PublishPackageHistorySerializer, DeploymentPlanValidateSerializer, DeploymentImportSerializer, DeploymentPlanListSerializer, ExecutionRecordSerializer, DeleteComponentSerializer, DeleteProDuctSerializer, ProductCompositionSerializer ) from backups.backups_utils import cmd from omp_server.settings import PROJECT_DIR from app_store.app_store_serializers import ( ProductDetailSerializer, ApplicationDetailSerializer ) from app_store import tmp_exec_back_task from utils.common.exceptions import OperateError from utils.common.views import BaseDownLoadTemplateView from app_store.tasks import publish_entry from rest_framework.filters import OrderingFilter, SearchFilter from utils.parse_config import ( BASIC_ORDER, AFFINITY_FIELD ) from app_store.new_install_utils import DataJson from app_store.new_install_utils import ServiceArgsPortUtils logger = logging.getLogger("server") class AppStoreListView(GenericViewSet, ListModelMixin): """ 应用商店 list 视图类 """ def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) name_field = kwargs.get("name_field") # 根据名称进行去重 result_ls, name_set = [], set() for obj in queryset: name = getattr(obj, name_field) if name not in name_set: name_set.add(name) result_ls.append(obj) serializer = self.get_serializer( self.paginate_queryset(result_ls), many=True) return self.get_paginated_response(serializer.data) class LabelListView(GenericViewSet, ListModelMixin): """ list: 查询所有标签列表 """ queryset = Labels.objects.all() # 过滤,排序字段 filter_backends = (DjangoFilterBackend,) filter_class = LabelFilter # 操作信息描述 get_description = "查询所有标签列表" def list(self, request, *args, **kwargs): query_set = self.get_queryset() # 过滤掉子项为 null 的 label label_type = request.query_params.get("label_type", -1) if int(label_type) == Labels.LABEL_TYPE_COMPONENT: query_set = Labels.objects.filter( applicationhub__app_type=ApplicationHub.APP_TYPE_COMPONENT) if int(label_type) == Labels.LABEL_TYPE_APPLICATION: query_set = Labels.objects.exclude( producthub__isnull=True) query_set = query_set.order_by( "id").values_list("label_name", flat=True).distinct() queryset = self.filter_queryset(query_set) return Response(list(queryset)) class ComponentListView(AppStoreListView): """ list: 查询所有基础组件列表 """ queryset = ApplicationHub.objects.filter( app_type=ApplicationHub.APP_TYPE_COMPONENT, is_release=True, ).order_by("-created") serializer_class = ComponentListSerializer pagination_class = PageNumberPager # 过滤,排序字段 filter_backends = (DjangoFilterBackend,) filter_class = ComponentFilter # 操作信息描述 get_description = "查询所有基础组件列表" def list(self, request, *args, **kwargs): return super(ComponentListView, self).list( request, name_field="app_name", *args, **kwargs) class ServiceListView(AppStoreListView): """ list: 查询所有应用服务列表 """ queryset = ProductHub.objects.filter( is_release=True).order_by("-created") serializer_class = ServiceListSerializer pagination_class = PageNumberPager # 过滤,排序字段 filter_backends = (DjangoFilterBackend,) filter_class = ServiceFilter # 操作信息描述 get_description = "查询所有应用服务列表" def list(self, request, *args, **kwargs): return super(ServiceListView, self).list( request, name_field="pro_name", *args, **kwargs) class UploadPackageView(GenericViewSet, CreateModelMixin): """ create: 上传安装包 """ queryset = UploadPackageHistory.objects.all() serializer_class = UploadPackageSerializer # 操作信息描述 post_description = "上传安装包" class RemovePackageView(GenericViewSet, CreateModelMixin): """ post: 批量移除安装包 """ queryset = UploadPackageHistory.objects.all() serializer_class = RemovePackageSerializer # 操作信息描述 post_description = "移除安装包" class ComponentDetailView(GenericViewSet, ListModelMixin): """ 查询组件详情 """ serializer_class = ApplicationDetailSerializer # 操作信息描述 get_description = "查询组件详情" def list(self, request, *args, **kwargs): arg_app_name = request.GET.get('app_name') queryset = ApplicationHub.objects.filter( app_name=arg_app_name).order_by("created") serializer = self.get_serializer(queryset, many=True) result = dict() result.update( { "app_name": arg_app_name, "versions": list(serializer.data) } ) return Response(result) class ServiceDetailView(GenericViewSet, ListModelMixin): """ 查询服务详情 """ serializer_class = ProductDetailSerializer # 操作信息描述 get_description = "查询服务详情" def list(self, request, *args, **kwargs): arg_pro_name = request.GET.get('pro_name') queryset = ProductHub.objects.filter( pro_name=arg_pro_name).order_by("created") serializer = self.get_serializer(queryset, many=True) result = dict() result.update( { "pro_name": arg_pro_name, "versions": list(serializer.data) } ) return Response(result) class ServicePackPageVerificationView(GenericViewSet, ListModelMixin): queryset = UploadPackageHistory.objects.filter( is_deleted=False, package_parent__isnull=True) serializer_class = UploadPackageHistorySerializer filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = UploadPackageHistoryFilter class PublishViewSet(ListModelMixin, CreateModelMixin, GenericViewSet): """ create: 发布接口 """ queryset = UploadPackageHistory.objects.filter(is_deleted=False, package_parent__isnull=True, package_status__in=[3, 4, 5]) serializer_class = PublishPackageHistorySerializer filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = PublishPackageHistoryFilter post_description = "上传应用商店安装包发布" def create(self, request, *args, **kwargs): params = request.data uuid = params.pop('uuid', None) if not uuid: raise OperateError("请传入uuid") publish_entry.delay(uuid) return Response({"status": "发布任务下发成功"}) class ExecuteLocalPackageScanView(GenericViewSet, CreateModelMixin): """ post: 扫描服务端执行按钮 """ serializer_class = ExecuteLocalPackageScanSerializer # 操作信息描述 post_description = "扫描本地安装包" def create(self, request, *args, **kwargs): """ post: 扫描服务端执行按钮 """ _uuid, _package_name_lst = tmp_exec_back_task.back_end_verified_init( operation_user=request.user.username ) ret_data = { "uuid": _uuid, "package_names": _package_name_lst } return Response(ret_data) class LocalPackageScanResultView(GenericViewSet, ListModelMixin): """ list: 扫描服务端执行结果查询接口 参数: uuid: 操作唯一uuid package_names: 安装包名称组成的字符串,英文逗号分隔 """ @staticmethod def get_res_data(operation_uuid, package_names): """ 获取安装包扫描状态 :param operation_uuid: 唯一操作uuid :param package_names: 安装包名称组成的字符串 :return: """ package_names_lst = package_names.split(",") # 确定当前安装包状态 if UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status=1 ).count() == len(package_names_lst): # 当全部安装包的状态为 1 - 校验失败 时,整个流程结束 stage_status = "check_all_failed" elif UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status__gt=2 ).exists(): # 当有安装包进入到分布流程时,整个流程进入到发布流程 if UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status=5 ).exists(): # 如果有安装包处于发布中装太,那么整个流程处于发布中状态 stage_status = "publishing" else: stage_status = "published" else: # 校验中 stage_status = "checking" package_info_dic = { el: { "status": 2, "message": "" } for el in package_names_lst } queryset = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, ) # 发布安装包状态及error信息提取 for item in queryset: package_info_dic[item.package_name]["status"] = item.package_status package_info_dic[item.package_name]["message"] = item.error_msg if stage_status == "checking": count = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status=UploadPackageHistory.PACKAGE_STATUS_PARSING ).count() message = f"共扫描到 {len(package_names_lst)} 个安装包," \ f"正在校验中..." \ f"({len(package_names_lst) - count}/{len(package_names_lst)})" elif stage_status == "check_all_failed": message = f"共计 {len(package_names_lst)} 个安装包校验失败!" elif stage_status == "publishing": _count = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status__gt=2 ).count() message = f"本次共发布 {_count} 个安装包,正在发布中..." elif stage_status == "published": _count = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_names_lst, package_status=3 ).count() message = \ f"本次共发布成功 {_count} 个安装包," \ f"发布失败 {len(package_names_lst) - _count} 个安装包!" else: message = "" package_detail_lst = list() for item in package_names_lst: package_detail_lst.append(package_info_dic[item]) ret_dic = { "uuid": operation_uuid, "package_names_lst": package_names_lst, "package_detail": package_detail_lst, "message": message, "stage_status": stage_status } return ret_dic @staticmethod def check_request_param(operation_uuid, package_names): """ 校验参数 :param operation_uuid: 唯一uuid :param package_names: 安装包名称 :return: """ if not operation_uuid: raise ValidationError({"uuid": "请求参数中必须包含 [uuid] 字段"}) if not package_names: raise ValidationError( {"package_names": "请求参数中必须包含 [package_names] 字段"}) def list(self, request, *args, **kwargs): operation_uuid = request.query_params.get("uuid", "") package_names = request.query_params.get("package_names", "") self.check_request_param(operation_uuid, package_names) res = self.get_res_data(operation_uuid, package_names) return Response(res) class ApplicationTemplateView(BaseDownLoadTemplateView): """ list: 获取应用商店下载模板 """ # 操作描述信息 get_description = "应用商店下载组件模板" def list(self, request, *args, **kwargs): return super(ApplicationTemplateView, self).list( request, template_file_name="app_publish_readme.md", *args, **kwargs) class DeploymentOperableView(GenericViewSet, ListModelMixin): """ list: 部署计划是否可操作 """ queryset = Service.objects.filter( service__is_base_env=False) # 操作描述信息 get_description = "查看部署计划" def list(self, request, *args, **kwargs): return Response(not self.get_queryset().exists()) class DeploymentTemplateView(BaseDownLoadTemplateView): """ list: 获取部署计划模板 """ # 操作描述信息 get_description = "获取部署计划模板" def list(self, request, *args, **kwargs): return super(DeploymentTemplateView, self).list( request, template_file_name="deployment.xlsx", *args, **kwargs) class DeploymentPlanListView(GenericViewSet, ListModelMixin): """ list: 查看部署计划 """ queryset = DeploymentPlan.objects.all().order_by("-created") serializer_class = DeploymentPlanListSerializer pagination_class = PageNumberPager # 操作描述信息 get_description = "查看部署计划" class DeploymentPlanValidateView(GenericViewSet, CreateModelMixin): """ create: 校验部署计划服务数据 """ serializer_class = DeploymentPlanValidateSerializer # 操作描述信息 post_description = "校验部署计划服务数据" def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): logger.error(f"deployment plan validate failed:{request.data}") raise ValidationError("数据格式错误") return Response(serializer.validated_data.get("result_dict")) class DeploymentPlanImportView(GenericViewSet, CreateModelMixin): """ create: 部署计划导入,服务数据入库 """ serializer_class = DeploymentImportSerializer # 操作描述信息 post_description = "部署计划导入,服务数据入库" @staticmethod def _get_app_pro_queryset(service_name_ls): """ 获取 app、pro 最新 queryset """ # 查询所有 application 信息 _queryset = ApplicationHub.objects.filter( app_name__in=service_name_ls, is_release=True) # 所有 application 默认取最新 new_app_id_list = [] for app in _queryset: new_version = _queryset.filter( app_name=app.app_name ).order_by("-created").first().app_version if new_version == app.app_version: new_app_id_list.append(app.id) app_queryset = _queryset.filter( id__in=new_app_id_list, is_release=True ).select_related("product") # 获取 application 对应的 product 信息 app_now = app_queryset.exclude(product__isnull=True) pro_id_list = app_now.values_list("product_id", flat=True).distinct() # 验证 product 的依赖项均已包含 pro_queryset = ProductHub.objects.filter(id__in=pro_id_list) return app_queryset, pro_queryset @staticmethod def _add_service(service_obj_ls, host_obj, app_obj, env_obj, only_dict, cluster_dict, product_dict, service_set, is_base_env=False, vip=None, role=None): """ 添加服务 """ # 切分 ip 字段,构建服务实例名 ip_split_ls = host_obj.ip.split(".") service_name = app_obj.app_name service_instance_name = f"{service_name}-{ip_split_ls[-2]}-{ip_split_ls[-1]}" # 当服务实例已经存在,则跳过 if service_instance_name in service_set: return service_set.add(service_instance_name) # 服务端口 service_port = json.dumps(ServiceArgsPortUtils().get_app_port(app_obj)) if not service_port: service_port = json.dumps([]) # 获取服务的基础目录、用户名、密码、密文 base_dir = "/data" username, password, password_enc = "", "", "" app_install_args = ServiceArgsPortUtils().get_app_install_args(app_obj) for item in app_install_args: if item.get("key") == "base_dir": base_dir = os.path.join( host_obj.data_folder, item.get("default", "").lstrip("/") ) elif item.get("key") == "username": username = item.get("default") elif item.get("key") == "password": password = item.get("default") elif item.get("key") == "password_enc": password_enc = item.get("default") else: pass # 拼接服务的控制脚本 service_controllers = {} app_controllers = json.loads(app_obj.app_controllers) for k, v in app_controllers.items(): if v != "": service_controllers[k] = f"{base_dir}/{v}" if "post_action" in app_obj.extend_fields: service_controllers["post_action"] = os.path.join( base_dir, app_obj.extend_fields.get("post_action") ) # 创建服务连接信息表 connection_obj = None if any([username, password, password_enc]): connection_obj, _ = ServiceConnectInfo.objects.get_or_create( service_name=service_name, service_username=username, service_password=password, service_password_enc=password_enc, service_username_enc="", ) # 集群信息 cluster_id = None if not is_base_env: if service_name in only_dict: # 存在于单实例字典中,删除单实例字典中数据,创建集群 only_dict.pop(service_name) upper_key = ''.join(random.choice( string.ascii_uppercase) for _ in range(7)) cluster_obj = ClusterInfo.objects.create( cluster_service_name=service_name, cluster_name=f"{service_name}-cluster-{upper_key}", service_connect_info=connection_obj, ) # 写入集群字典,记录 id cluster_dict[service_name] = ( cluster_obj.id, cluster_obj.cluster_name) elif service_name in cluster_dict: # 存在于集群字典中 pass else: # 尚未记录,加入单实例字典 only_dict[service_name] = service_instance_name # 如果产品信息不再字典中,将其添加至字典 if app_obj.product and \ app_obj.product.pro_name not in product_dict: product_dict[app_obj.product.pro_name] = app_obj.product # 添加服务到列表中 service_obj_ls.append(Service( ip=host_obj.ip, service_instance_name=service_instance_name, service=app_obj, service_port=service_port, service_controllers=service_controllers, service_status=Service.SERVICE_STATUS_READY, env=env_obj, service_connect_info=connection_obj, cluster_id=cluster_id, vip=vip, service_role=role, )) def create(self, request, *args, **kwargs): # 信任数据,只进行格式校验 serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): logger.error(f"host batch import failed:{request.data}") raise ValidationError("数据格式错误") # env 环境对象 default_env = Env.objects.filter(id=1).first() # 实例名称、服务数据、服务名称列表 instance_info_ls = serializer.data.get("instance_info_ls") instance_name_ls = list( map(lambda x: x.get("instance_name"), instance_info_ls)) service_data_ls = serializer.data.get("service_data_ls") service_name_ls = list( map(lambda x: x.get("service_name"), service_data_ls)) # 亲和力 tengine 字段 tengine_name = AFFINITY_FIELD.get("tengine", "tengine") # 主机、基础环境 queryset host_queryset = Host.objects.filter(instance_name__in=instance_name_ls) base_env_queryset = ApplicationHub.objects.filter(is_base_env=True) # 应用、产品 queryset app_queryset, pro_queryset = self._get_app_pro_queryset( service_name_ls) # 如果主机不存在 if not host_queryset.exists(): raise OperateError("导入失败,主机未纳管") # 构建 uuid operation_uuid = serializer.data.get("operation_uuid") if \ serializer.data.get("operation_uuid") else uuid.uuid4() # 考虑到录入主机可能大于分配服务主机,故此记录真实使用的主机实例数量 service_instance_set = set( map(lambda x: x.get("instance_name"), service_data_ls)) use_host_queryset = host_queryset.filter( instance_name__in=service_instance_set) try: # 服务对象列表、基础环境字典 service_obj_ls = [] base_env_dict = {} # 产品信息字典 product_dict = {} # 单实例、集群服务字典 only_dict, cluster_dict = {}, {} # tengine 所在主机对象字典 tengine_host_obj_dict = {} # 服务实例唯一集合,用于限制重复服务 service_set = set() # 遍历获取所有需要安装的服务 for service_data in service_data_ls: instance_name = service_data.get("instance_name") service_name = service_data.get("service_name") # 服务的角色、虚拟IP vip = service_data.get("vip") role = service_data.get("role") # 主机、应用对象 host_obj = use_host_queryset.filter( instance_name=instance_name).first() app_obj = app_queryset.filter(app_name=service_name).first() # 亲和力为 tengine 字段 (Web 服务) 跳过,后续按照 product 维度补充 if app_obj.extend_fields.get("affinity") == tengine_name: continue # 如果服务为 tengine 时,记录其所在节点 if app_obj.app_name == tengine_name: tengine_host_obj_dict[host_obj.ip] = host_obj # 检查服务依赖 if app_obj.app_dependence: dependence_list = json.loads(app_obj.app_dependence) for dependence in dependence_list: app_name = dependence.get("name") # version = dependence.get("version") # base_env_obj = base_env_queryset.filter( # app_name=app_name, app_version__startswith=version # ).order_by("-created").first() base_env_obj = base_env_queryset.filter( app_name=app_name).order_by("-created").first() # 如果服务的依赖中有 base_env,并且对应 ip 上不存在则写入 if base_env_obj and \ app_name not in base_env_dict.get(host_obj.ip, []): # base_env 一定为单实例 self._add_service( service_obj_ls, host_obj, base_env_obj, default_env, only_dict, cluster_dict, product_dict, service_set, is_base_env=True) # 以 ip 为维度记录,避免重复 if host_obj.ip not in base_env_dict: base_env_dict[host_obj.ip] = [] base_env_dict[host_obj.ip].append(app_name) # 添加服务 self._add_service( service_obj_ls, host_obj, app_obj, default_env, only_dict, cluster_dict, product_dict, service_set, vip=vip, role=role) # 亲和力为 tengine 字段 (Web 服务) 列表 app_target = ApplicationHub.objects.filter( product__in=pro_queryset) tengine_app_list = list(filter( lambda x: x.extend_fields.get("affinity") == tengine_name, app_target)) # 为所有 tengine 节点添加亲和力服务 for tengine_ip, host_obj in tengine_host_obj_dict.items(): for app_obj in tengine_app_list: self._add_service( service_obj_ls, host_obj, app_obj, default_env, only_dict, cluster_dict, product_dict, service_set) service_instance_name_ls = list(map( lambda x: x.service_instance_name, service_obj_ls)) # run_user 字典 run_user_dict = {} for instance_info in instance_info_ls: if instance_info.get("run_user", "") != "": run_user_dict[ instance_info.get("instance_name") ] = instance_info.get("run_user") # 服务 memory 字典 service_memory_dict = {} for service_data in service_data_ls: if service_data.get("memory", "") != "": service_memory_dict[ service_data.get("service_name") ] = service_data.get("memory") # 数据库入库 with transaction.atomic(): # 已安装产品信息 product_obj_ls = [] for pro_name, pro_obj in product_dict.items(): upper_key = ''.join(random.choice( string.ascii_uppercase) for _ in range(7)) product_obj_ls.append(Product( product_instance_name=f"{pro_name}-{upper_key}", product=pro_obj )) Product.objects.bulk_create(product_obj_ls) # 批量创建 service,return 无 id,需重查获取 Service.objects.bulk_create(service_obj_ls) # 为所有服务统一补充集群信息 for k, v in cluster_dict.items(): Service.objects.filter( service__app_name=k ).update(cluster_id=v[0]) # 获取所有服务对象 service_queryset = Service.objects.filter( service_instance_name__in=service_instance_name_ls ).select_related("service") # 遍历服务,如果存在依赖信息则补充 for service_obj in service_queryset: service_dependence_list = [] if service_obj.service.app_dependence: dependence_list = json.loads( service_obj.service.app_dependence) for dependence in dependence_list: app_name = dependence.get("name") item = { "name": app_name, "cluster_name": None, "instance_name": None, } if app_name in only_dict: item["instance_name"] = only_dict.get(app_name) elif app_name in cluster_dict: item["cluster_name"] = cluster_dict.get( app_name)[1] else: # base_env 不在单实例和集群列表中 ip_split_ls = service_obj.ip.split(".") service_instance_name = f"{app_name}-{ip_split_ls[-2]}-{ip_split_ls[-1]}" item["instance_name"] = service_instance_name service_dependence_list.append(item) service_obj.service_dependence = json.dumps( service_dependence_list) service_obj.save() # 更新主机非base_env服务数量 for host_obj in use_host_queryset: obj_service_num = service_queryset.filter( ip=host_obj.ip).exclude(service__is_base_env=True).count() Host.objects.filter( id=host_obj.id ).update(service_num=obj_service_num) # 主安装记录表、后续任务记录表 main_history_obj = MainInstallHistory.objects.create( operator=request.user.username, operation_uuid=operation_uuid, ) PostInstallHistory.objects.create( main_install_history=main_history_obj, ) # 主机层安装记录表 pre_install_obj_ls = [] for host_obj in use_host_queryset: pre_install_obj_ls.append(PreInstallHistory( main_install_history=main_history_obj, ip=host_obj.ip, )) PreInstallHistory.objects.bulk_create(pre_install_obj_ls) # 构建基础组件多维列表 component_order_ls = [[] for _ in range(len(BASIC_ORDER))] for k, v in BASIC_ORDER.items(): for i in range(len(v)): component_order_ls[k].append([]) # 用于详情表排序的列表 component_last_ls = [] service_order_ls = [] service_last_ls = [] # 安装详情表 for service_obj in service_queryset: # 获取主机对象 host_obj = use_host_queryset.filter( ip=service_obj.ip).first() app_args = ServiceArgsPortUtils().get_app_install_args( service_obj.service ) # 获取服务对应的 run_user 和 memory run_user = run_user_dict.get(host_obj.instance_name, None) memory = service_memory_dict.get( service_obj.service.app_name, None) # 如果用户自定义 run_user、memory 需覆盖写入 install_args if run_user: for i in app_args: if i.get("key") == "run_user": i["default"] = run_user break else: app_args.append({ "name": "安装用户", "key": "run_user", "default": run_user, }) if memory: for i in app_args: if i.get("key") == "memory": i["default"] = memory break else: app_args.append({ "name": "运行内存", "key": "memory", "default": memory, }) # {data_path} 占位符替换 for i in app_args: if "dir_key" in i: i["default"] = os.path.join( host_obj.data_folder, i.get("default", "").lstrip("/") ) # 标记服务是否需要 post post_action_flag = 4 if service_obj.service.extend_fields.get( "post_action", "") != "": post_action_flag = 0 # 服务端口 service_port = ServiceArgsPortUtils().get_app_port( service_obj.service ) # 构建 detail_install_args detail_install_args = { "ip": service_obj.ip, "name": service_obj.service.app_name, "ports": service_port, "version": service_obj.service.app_version, "run_user": "", "data_folder": host_obj.data_folder, "cluster_name": None, "install_args": app_args, "instance_name": service_obj.service_instance_name } detail_obj = DetailInstallHistory( service=service_obj, main_install_history=main_history_obj, install_detail_args=detail_install_args, post_action_flag=post_action_flag, ) # 安装详情表按顺序录入 app_type = service_obj.service.app_type # 公共组件 if app_type == ApplicationHub.APP_TYPE_COMPONENT: for k, v in BASIC_ORDER.items(): if service_obj.service.app_name in v: # 动态根据层级插入数据 target = v.index(service_obj.service.app_name) component_order_ls[k][target].append( detail_obj) break else: component_last_ls.append(detail_obj) elif app_type == ApplicationHub.APP_TYPE_SERVICE: app_level_str = service_obj.service.extend_fields.get( "level", "") if app_level_str == "": service_last_ls.append(detail_obj) else: k = int(app_level_str) # 动态根据层级索引创建空列表 if len(service_order_ls) <= k: for i in range(len(service_order_ls), k + 1): service_order_ls.append([]) service_order_ls[k].append(detail_obj) else: pass # 合并多维列表和后置列表 detail_history_obj_ls = [] for child_ls in component_order_ls: for i in child_ls: if len(i) != 0: detail_history_obj_ls += i detail_history_obj_ls += component_last_ls for i in service_order_ls: detail_history_obj_ls += i detail_history_obj_ls += service_last_ls DetailInstallHistory.objects.bulk_create(detail_history_obj_ls) # 部署计划表 DeploymentPlan.objects.create( plan_name=f"快速部署-{str(int(round(time.time() * 1000)))}", host_num=use_host_queryset.count(), product_num=pro_queryset.count(), service_num=len(service_data_ls), create_user=request.user.username, operation_uuid=operation_uuid, ) # 生成 data.json data_json = DataJson( operation_uuid=str(operation_uuid), service_obj=service_queryset) data_json.run() except Exception as err: logger.error(f"import deployment plan err: {err}") import traceback logger.error(traceback.print_exc()) raise OperateError(f"导入执行计划失败: {err}") return Response({ "operation_uuid": operation_uuid, "host_num": host_queryset.count(), "product_num": pro_queryset.count(), "service_num": len(service_data_ls), }) class ExecutionRecordAPIView(GenericViewSet, ListModelMixin): queryset = ExecutionRecord.objects.exclude(count=0).all() pagination_class = PageNumberPager filter_backends = (OrderingFilter, SearchFilter) search_fields = ("module",) ordering_fields = ("created",) ordering = ('-created',) serializer_class = ExecutionRecordSerializer # 操作信息描述 get_description = "查询执行记录" class ProductCompositionView(GenericViewSet, ListModelMixin, CreateModelMixin): serializer_class = ProductCompositionSerializer # 关闭权限、认证设置 authentication_classes = () permission_classes = () get_description = "查询产品信息" post_description = "修改产品包含服务信息" def get_queryset(self): return ProductHub.objects.filter(**self.request.query_params.dict()) def list(self, request, *args, **kwargs): serializer = self.get_serializer(self.get_queryset(), many=True) for data in serializer.data: data["pro_services"] = json.loads(data.get("pro_services")) return Response(serializer.data) def create(self, request, *args, **kwargs): pro_services = json.dumps(request.data.get("pro_services", []), ensure_ascii=False) request.data["pro_services"] = pro_services serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) params = request.data pro_obj = ProductHub.objects.filter( pro_name=params.get('pro_name'), pro_version=params.get('pro_version') ).first() pro_obj.pro_services = pro_services pro_obj.save() return Response("修改成功") class DeleteAppStorePackageView(GenericViewSet, ListModelMixin, CreateModelMixin): """ get: 查看应用商店 """ get_description = "查看应用商店" post_description = "删除应用商店" def get_queryset(self): if self.request.query_params.get('type') == "component": return ApplicationHub.objects.filter( app_type=ApplicationHub.APP_TYPE_COMPONENT).order_by("-created") else: return ProductHub.objects.all().order_by("-created") def get_serializer_class(self): if self.request.query_params.get('type') == "component": return DeleteComponentSerializer return DeleteProDuctSerializer def list(self, request, *args, **kwargs): app_type = request.GET.get("type") if not app_type or app_type not in ["component", "product"]: raise OperateError("请传入type或合法type") queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer(queryset, many=True) app_name_dc = {} for _ in serializer.data: app_name_dc.setdefault(_["name"], []).extend(_["versions"]) res_ls = [] for name, versions in app_name_dc.items(): res_ls.append({"name": name, "versions": versions}) return Response( {"data": res_ls, "type": app_type} ) @staticmethod def explain_info(): app_ls = ApplicationHub.objects.all().values_list( "id", "app_name", "app_version", "product") pro_ls = ProductHub.objects.all().values_list("id", "pro_name", "pro_version") app_dc = {} pro_id_app_count = {} for app in app_ls: app_dc[f"{app[1]}|{app[2]}"] = app[0] if app[3]: pro_id_app_count[app[3]] = pro_id_app_count.get(app[3], 0) + 1 pro_id_count = {} for pro in pro_ls: if pro_id_app_count.get(pro[0]): pro_id_count[f"{pro[1]}|{pro[2]}"] = [ pro_id_app_count[pro[0]], pro[0]] else: pro_id_count[f"{pro[1]}|{pro[2]}"] = [0, pro[0]] return pro_id_count, app_dc def check_service(self, params): pro_id_count, app_dc = self.explain_info() ser_id = [] pro_dc = {} for info in params["data"]: if params['type'] == "component": for version in info["versions"]: app_id = app_dc.get(f'{info["name"]}|{version}') if app_id: ser_id.append(app_id) else: # 查询选择的产品是不是勾选全部 pro_info = pro_id_count[info["name"]] # 确定就是产品删除 if pro_info[0] == len(info["versions"]): pro_dc[pro_info[1]] = info["name"] for _ in info["versions"]: app_id = app_dc.get(_) if app_id: ser_id.append(app_id) ser_name = Service.objects.filter( service__in=ser_id).values_list('service_instance_name', flat=True) if ser_name: raise OperateError(f'存在已安装的服务{",".join(ser_name)}') return ser_id, pro_dc @staticmethod def del_file(file_path): logger.info(f"应用包可能删除的路径 {file_path}") if file_path and len(file_path) > 28: _out, _err, _code = cmd(f"/bin/rm -rf {file_path}") if _code != 0: raise OperateError(f'执行cmd异常,删除路径失败{file_path}:{_err},{_out}') def del_database(self, ser_id, pro_dc): app_objs = ApplicationHub.objects.filter(id__in=ser_id) del_ser_file = [] for app in app_objs: if app.app_package.package_name: del_ser_file.append(os.path.join( PROJECT_DIR, "package_hub", app.app_package.package_path, app.app_package.package_name, )) self.del_file(" ".join(del_ser_file)) app_objs.delete() if pro_dc: del_pro_file = [] for pro_id, pro_info in pro_dc.items(): pro_info = pro_info.replace("|", "-") if pro_info: del_pro_file.append( os.path.join(PROJECT_DIR, f"package_hub/verified/{pro_info}")) self.del_file(" ".join(del_pro_file)) ProductHub.objects.filter(id__in=list(pro_dc)).delete() def create(self, request, *args, **kwargs): params = request.data app_type = params.get('type', None) if not app_type: raise OperateError("请传入类型") ser_id, pro_dc = self.check_service(params) self.del_database(ser_id, pro_dc) return Response({"status": "删除成功"}) ================================================ FILE: omp_server/app_store/views_for_install.py ================================================ # -*- coding: utf-8 -*- # Project: views_for_install # Author: jon.liu@yunzhihui.com # Create time: 2021-10-21 17:59 # IDE: PyCharm # Version: 1.0 # Introduction: from rest_framework import status from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ( ListModelMixin, CreateModelMixin ) from django_filters.rest_framework.backends import DjangoFilterBackend from db_models.models import ( ApplicationHub, ProductHub, MainInstallHistory, Service ) from app_store.app_store_serializers import ( ComponentEntranceSerializer, ProductEntranceSerializer, ExecuteInstallSerializer, InstallHistorySerializer, ServiceInstallHistorySerializer ) from app_store.app_store_filters import ( ComponentEntranceFilter, ProductEntranceFilter, InstallHistoryFilter, ServiceInstallHistoryFilter ) class ComponentEntranceView(GenericViewSet, ListModelMixin): """ list: 组件安装入口 """ queryset = ApplicationHub.objects.filter( is_release=True, app_type=ApplicationHub.APP_TYPE_COMPONENT ).order_by("-created") serializer_class = ComponentEntranceSerializer filter_backends = (DjangoFilterBackend, ) filter_class = ComponentEntranceFilter get_description = "获取组件安装数据入口" def list(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer(queryset, many=True) # 对返回数据重新进行处理,对process_continue字段进行提取 for el in serializer.data: if len(el.get("app_dependence", [])) != 0: for item in el.get("app_dependence", []): if "process_continue" in item and \ not item["process_continue"] and \ el.get("process_continue"): el["process_continue"] = False el["process_message"] = item.get("process_message") break return Response(serializer.data) class ProductEntranceView(GenericViewSet, ListModelMixin): """ list: 产品、应用安装入口 """ queryset = ProductHub.objects.filter( is_release=True, ).order_by("-created") serializer_class = ProductEntranceSerializer filter_backends = (DjangoFilterBackend, ) filter_class = ProductEntranceFilter get_description = "获取产品应用安装数据入口" class ExecuteInstallView(GenericViewSet, CreateModelMixin): serializer_class = ExecuteInstallSerializer post_description = "执行安装入口接口" def create(self, request, *args, **kwargs): """ post: 执行安装按钮接口 """ serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) headers = self.get_success_headers(serializer.data) return Response( serializer.data, status=status.HTTP_201_CREATED, headers=headers ) class InstallHistoryView(GenericViewSet, ListModelMixin): """ list: 获取安装记录 """ queryset = MainInstallHistory.objects.all().order_by("-created") serializer_class = InstallHistorySerializer filter_backends = (DjangoFilterBackend,) filter_class = InstallHistoryFilter get_description = "获取安装历史数据入口" class ServiceInstallHistoryDetailView(GenericViewSet, ListModelMixin): """ list: 获取安装记录 """ queryset = Service.objects.all().order_by("-created") serializer_class = ServiceInstallHistorySerializer filter_backends = (DjangoFilterBackend,) filter_class = ServiceInstallHistoryFilter get_description = "获取单个服务安装记录" ================================================ FILE: omp_server/backups/__init__.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- ================================================ FILE: omp_server/backups/admin.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- ================================================ FILE: omp_server/backups/apps.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- from django.apps import AppConfig class BackupsConfig(AppConfig): name = 'backups' ================================================ FILE: omp_server/backups/backup_service.py ================================================ # # -*- coding:utf-8 -*- # # Project: backup_service import logging import random import time import json """ if __name__ == '__main__': import django CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(CURRENT_DIR) PYTHON_PATH = os.path.join(PROJECT_DIR, "component/env/bin/python3") MANAGE_PATH = os.path.join(PROJECT_DIR, "omp_server/manage.py") sys.path.append(os.path.join(PROJECT_DIR, "omp_server")) # 加载Django环境 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "omp_server.settings") django.setup() from db_models.models.install import DetailInstallHistory from utils.plugin.salt_client import SaltClient logger = logging.getLogger("server") class BackupDB(object): def __init__(self): self.salt_client = SaltClient() self.timeout = 300 self.service_names = [] self.upload_real_paths = [] @staticmethod def get_backup_info(service_obj): try: service_dict = { "cw_o_app_name": service_obj.service.app_name, "cw_o_ip": service_obj.ip, "tmp": time.strftime("%Y%m%d%H%M%S", time.localtime()) + str(random.randint(1000, 9999)), } # 连接信息 service_connect_info = service_obj.service_connect_info if service_connect_info: service_dict.update( { "cw_o_username": service_connect_info.service_username, "cw_o_password": service_connect_info.service_password, } ) # 端口 port_json = json.loads(service_obj.service_port) for k_port in port_json: service_dict.update({ f"cw_o_{k_port.get('key', '')}": k_port.get("default", "") }) # 安装参数 obj = DetailInstallHistory.objects.filter(service_id=service_obj.id).first() install_args = obj.install_detail_args.get("install_args", {}) for args in install_args: service_dict.update({ f"cw_o_{args.get('key', '')}": args.get("default", "") }) return service_dict except Exception as e: logger.error(f"获取信息失败请查看service或其账号密码端口是否存在{e}") def backup_service(self, back_id): back_obj = BackupHistory.objects.filter(id=back_id).first() service_instances = back_obj.content omp_backup_dir = back_obj.retain_path for si in service_instances: service_obj = Service.objects.filter( service_instance_name=si).first() service_dict = self.get_backup_info(service_obj) cmd_str = {} if isinstance(service_dict, dict): if service_obj.service.app_name == "mysql": cmd_str = "test -d {backup_dir} || mkdir -p {backup_dir}&& " \ "chown -R {run_user}:{run_user} {backup_dir} &&" \ " {app_dir}/bin/mysqldump --single-transaction " \ "-P{service_port} -u{service_user} {service_pass} -h'127.0.0.1' --all-databases > " \ "{backup_dir}/{service_name}-{ip}-{tmp}.sql".format( **service_dict) service_dict["file_name"] = "{service_name}-{ip}-{tmp}.sql".format( **service_dict) elif service_obj.service.app_name == "arangodb": cmd_str = "test -d {backup_dir}/{service_name}-{ip}-{tmp} || " \ "mkdir -p {backup_dir}/{service_name}-{ip}-{tmp}&& " \ "chown -R {run_user}:{run_user} {backup_dir} " \ "&& {app_dir}/bin/arangodump --server.endpoint " \ "tcp://127.0.0.1:{service_port} --server.username {service_user}" \ " --server.password {arangodb_pwd} --all-databases true " \ "--output-directory {backup_dir}/{service_name}-{ip}-{tmp}".format( **service_dict) service_dict["file_name"] = "{service_name}-{ip}-{tmp}".format( **service_dict) elif service_obj.service.app_name == "postgreSql": cmd_str = "test -d {backup_dir} || mkdir -p {backup_dir}&& " \ "chown -R {run_user}:{run_user} {backup_dir} &&" \ " {app_dir}/bin/pg_dumpall -U {run_user} -h'127.0.0.1' -p{service_port} > " \ "{backup_dir}/{service_name}-{ip}-{tmp}.sql".format( **service_dict) service_dict["file_name"] = "{service_name}-{ip}-{tmp}.sql".format( **service_dict) else: # TODO 应用不合法 pass backup_flag, backup_msg = self.salt_client.cmd( target=service_dict.get("ip"), command=(cmd_str,), timeout=self.timeout ) if not backup_flag: message = f'{service_dict.get("ip")}上{service_dict.get("service_name")}备份失败!' logger.error(f"{message}, 详情为:{backup_msg}") return False, message # 同步数据 sync_flag, sync_msg = self.sync_data_with_omp(service_dict) # print(cmd_str) if not sync_flag: message = f'{service_dict.get("ip")}上{service_dict.get("service_name")}备份失败!' logger.error(f"{message}, 详情为:{sync_msg}") return False, message # 3.将主节点的备份数据同步到备份目录 file_name = back_obj.file_name name = file_name.replace(".tar.gz", "") template_dir = os.path.join( omp_backup_dir, name) upload_real_paths = " ".join(self.upload_real_paths) if len(template_dir) <= 5 or len(upload_real_paths) <= 5: return False, "目录异常,防止保护文件丢失触发熔断." \ "template_dir:template_dir:{0},upload_real_paths:{1}".format( template_dir, upload_real_paths ) cmd_str = "test -d {0} || mkdir -p {0} && cp -rf {1} {0} && rm -rf {1} && cd {2}" \ " && tar -zcf {3} {4} && rm -rf {0}".format( template_dir, upload_real_paths, omp_backup_dir, file_name, name) _out, _err, _code = cmd( cmd_str ) logger.info(f"执行合并备份文件命令{cmd_str}") if int(_code) != 0: return False, _out logger.info(f"{''.join(self.service_names)}备份完成") return True, "Success" """ ================================================ FILE: omp_server/backups/backups_serializers.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- import logging import os from rest_framework.serializers import ModelSerializer from rest_framework import serializers from rest_framework.exceptions import ValidationError from utils.common.validators import ReValidator from db_models.models import BackupHistory, BackupSetting, BackupCustom logger = logging.getLogger("server") class BackupHistoryIdsSerializer(ModelSerializer): ids = serializers.ListField( child=serializers.IntegerField(), help_text="主机id列表", required=True, write_only=True, source="id", error_messages={"required": "必须包含[ids]字段"} ) def validate(self, attrs): backup_obj = BackupHistory.objects.filter(id__in=attrs["id"]) if len(backup_obj) != len(attrs["id"]): raise ValidationError(f"id不存在") attrs["id"] = backup_obj return attrs class Meta: model = BackupHistory fields = ("ids",) class BackupHistorySerializer(ModelSerializer): class Meta: model = BackupHistory fields = "__all__" write_only_fields = ("id",) class BackupCustomSerializer(ModelSerializer): notes = serializers.CharField( help_text="备注信息", required=False, default="", allow_null=True, allow_blank=True ) def validate(self, attrs): backup_obj = BackupCustom.objects.filter( field_k=attrs["field_k"], field_v=attrs["field_v"]).count() if backup_obj != 0: raise ValidationError(f"自定义参数不可重复") return attrs class Meta: model = BackupCustom fields = "__all__" class BackupCustomRepeatSerializer(ModelSerializer): name = serializers.SerializerMethodField() def get_name(self, obj): instance_ls = [] for instance in obj.backupsetting_set.all(): instance_ls.extend(instance.backup_instances) return ",".join(set(instance_ls)) class Meta: model = BackupCustom fields = ("name",) class BackupSettingSerializer(ModelSerializer): backup_instances = serializers.ListField( help_text="备份服务实例名称", required=True, error_messages={"required": "备份服务需必填"} ) crontab_detail = serializers.DictField( help_text="定时任务详情", required=True, error_messages={"required": "请填写备份策略"}) retain_day = serializers.IntegerField(help_text="文件保存天数", default=1) retain_path = serializers.CharField( help_text="文件保存路径", default="/data/omp/data/backup/", min_length=2, error_messages={ "required": "必须包含[retain_path]字段", "min_length": "用户名长度需小于{min_length}"}, validators=[ ReValidator(regex=r"^/[-_/a-zA-Z0-9]+$"), ] ) backup_custom = BackupCustomSerializer(many=True, required=False) def validate_retain_path(self, retain_path): # NOQA try: folder = os.path.exists(retain_path) if not folder: os.makedirs(retain_path) file_path = os.path.join(retain_path, 'test.txt') create_file = open(file_path, "w") create_file.close() except Exception as e: logger.info(f"校验文件夹权限失败:{str(e)}") raise ValidationError(f"请确定程序对备份文件保存文件夹{retain_path}有读写权限!") return retain_path def validate_crontab_detail(self, crontab_detail): # NOQA # # ToDo暂时不校验但有校验的必要 return crontab_detail class Meta: model = BackupSetting fields = ( "id", "backup_instances", "crontab_detail", "retain_day", "retain_path", "backup_custom", "is_on" ) ================================================ FILE: omp_server/backups/backups_utils.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- import logging import os import subprocess import tarfile import json import time import random from db_models.models import Service, Host, BackupHistory, DetailInstallHistory from omp_server.settings import PROJECT_DIR from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from utils.plugin.salt_client import SaltClient from utils.parse_config import \ THREAD_POOL_MAX_WORKERS, LOCAL_IP, TENGINE_PORT logger = logging.getLogger("server") def cmd(command): """执行本地shell命令""" p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout, stderr = p.communicate() _out, _err, _code = \ stdout.decode("utf8"), stderr.decode("utf8"), p.returncode return _out, _err, _code def tar_files(source_path, target_path): """ 压缩文件 可以压缩多个文件或者单个目录 source_path:源文件路径 list [a.tar.gz, b.tar.gz] target_path: 目标文件路径 """ success = True try: with tarfile.open(target_path, "w:gz") as tar: for file_path in source_path: if not os.path.exists(file_path): success = False tar.add(file_path) logging.info(f"{source_path}文件打包成功,压缩文件为{target_path}") except Exception as e: logging.error(f"{source_path}文件打包错误,error={e}") os.remove(target_path) return False if not success: os.remove(target_path) return False for file_path in source_path: os.remove(file_path) return True def transfer_week(day_of_week): """ 适配前端day_of_week参数传递 """ if day_of_week == '6': day_of_week = '0' elif day_of_week == '*': pass else: day_of_week = str(int(day_of_week) + 1) return day_of_week def exec_scripts(exec_json, host_i_d, his_id): # ToDo 考虑并发场景 tar_name = f"{exec_json['cw_o_app_name']}_bak.sh" scripts_path = os.path.join( PROJECT_DIR, f"package_hub/_modules/{tar_name}" ) try: salt_client = SaltClient() his_tmp = f"{scripts_path}{his_id}" cmd(f"cp {scripts_path}.tmp {his_tmp}") # 占位符替换 with open(his_tmp, 'r+') as f: t = f.read() for before, after in exec_json.items(): t = t.replace(("${%s}" % str(before)), str(after)) f.seek(0, 0) f.write(t) f.truncate() # 发送脚本 tar_ip = exec_json['cw_o_ip'] tar_package = f"{host_i_d[tar_ip]}/omp_packages/{tar_name}" is_success, message = salt_client.cp_file( target=tar_ip, source_path=f"_modules/{tar_name}{his_id}", target_path=tar_package ) if not is_success: return False, message # 执行脚本 cmd_str = f"nohup bash {tar_package} >" \ f" {host_i_d[tar_ip]}/omp_packages/logs" \ f"/{exec_json['cw_o_app_name']}{exec_json['tmp']}.log 2>&1 &" return salt_client.cmd(tar_ip, cmd_str, timeout=30) except Exception as e: return False, f"执行异常{e}" def change_status(obj, result, message=""): if result: obj.result = BackupHistory.SUCCESS else: obj.result = BackupHistory.FAIL obj.message = message obj.save() def check_result(future_list, his_ls): for index, future in enumerate(as_completed(future_list)): is_success, message = future.result() if not is_success: change_status(his_ls[index], False, message) def get_backup_info(service_obj): """ 获取备份配置所需变量,内置变量命名 cw_o_xxx """ try: service_dict = { "cw_o_app_name": service_obj.service.app_name, "cw_o_ip": service_obj.ip, "tmp": time.strftime("%Y%m%d%H%M%S", time.localtime()) + str(random.randint(1000, 9999)), } # 连接信息 service_connect_info = service_obj.service_connect_info if service_connect_info: service_dict.update( { "cw_o_username": service_connect_info.service_username, "cw_o_password": service_connect_info.service_password, } ) # 端口 port_json = json.loads(service_obj.service_port) for k_port in port_json: service_dict.update({ f"cw_o_{k_port.get('key', '')}": k_port.get("default", "") }) # 安装参数 obj = DetailInstallHistory.objects.filter(service_id=service_obj.id).first() install_args = obj.install_detail_args.get("install_args", {}) for args in install_args: service_dict.update({ f"cw_o_{args.get('key', '')}": args.get("default", "") }) return service_dict except Exception as e: logger.error(f"获取信息失败请查看service或其账号密码端口是否存在{e}") def backup_service_data(his_ls): """ 备份服务 :param his_ls: his表列表 :return: """ master_url = f"{LOCAL_IP}:{TENGINE_PORT.get('access_port', '19001')}" \ f"/api/backups/backupHistory" host_i_d = dict(Host.objects.all().values_list("ip", "data_folder")) with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: check_ls = [] for his in his_ls: service_obj = Service.objects.filter( service_instance_name=his.content).first() if not service_obj: logger.info(f"{his.content}服务已被卸载或不可用") continue backup_script_args = get_backup_info(service_obj) backup_script_args.update(his.extend_field) backup_script_args.update({ "cw_o_master_url": f"{master_url}/{his.id}/" }) future_obj = executor.submit( exec_scripts, backup_script_args, host_i_d, his.id) check_ls.append(future_obj) check_result(check_ls, his_ls) def rm_backend_file(his_objs): """ 删除过期文件 :param his_objs: BackupHistory his_objs :return: """ fail_files = [] for history in his_objs: if history.file_deleted or history.result == BackupHistory.FAIL: history.delete() continue expire_file = os.path.join(history.retain_path, history.file_name) try: os.remove(expire_file) except Exception as e: logger.error(f"删除备份文件{expire_file}失败: {str(e)}") fail_files.append(history.file_name) history.delete() return fail_files def check_ing(obj): if not obj: return True back_instance = BackupHistory.objects.filter( result=BackupHistory.ING).values_list("content", flat=True) ing_instance = set(obj.backup_instances) & set(back_instance) if ing_instance: logger.info(f"当前实例正在备份:{','.join(ing_instance)}") return True return False ================================================ FILE: omp_server/backups/migrations/__init__.py ================================================ ================================================ FILE: omp_server/backups/tasks.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- import os import time import datetime import logging from celery import shared_task from celery.utils.log import get_task_logger from utils.plugin.salt_client import SaltClient from backups.backups_utils import rm_backend_file, backup_service_data, \ cmd, change_status, check_ing from db_models.models import BackupSetting, BackupHistory from utils.plugin.crontab_utils import maintain # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") @shared_task @maintain def backup_service(**kwargs): """ 定时备份函数 :**kwargs: :return: """ # 判断setting是否存在,是否开启 backup_setting_id = kwargs.get("task_id") backup_setting = BackupSetting.objects.filter( id=backup_setting_id).first() if check_ing(backup_setting): return False if backup_setting.retain_day == -1: expire_time = None else: expire_time = datetime.datetime.now() + datetime.timedelta( days=backup_setting.retain_day) extend_field = backup_setting.backup_custom if extend_field: extend_field = dict(extend_field.all().values_list("field_k", "field_v")) his_ls = [] for instance in backup_setting.backup_instances: name = f"backup-{str(int(time.time()))}-{instance}" his_ls.append( BackupHistory.objects.create( backup_name=name, content=instance, expire_time=expire_time, message={}, retain_path=backup_setting.retain_path, file_name=f"{name}.tar.gz", extend_field=extend_field, ) ) # 调备份 backup_service_data(his_ls) # 如果有过期删过期, 更新file_deleted ToDo 考虑过期数据走向 histories = BackupHistory.objects.filter(expire_time__lte=datetime.datetime.now()).exclude(expire_time=None) if histories: rm_backend_file(histories) histories.delete() @shared_task def pull_back_file(his_id, remote_path, ip): """ 同步节点备份数据到omp所在机器 """ # 异步暂时想不到如何同步数据 his_obj = BackupHistory.objects.filter(id=his_id).first() salt_client = SaltClient() salt_data = salt_client.client.opts.get("root_dir") logger.info(f"获取文件ip:{ip},远端目录:{remote_path},本地目录:{his_obj.file_name}") cp_flag, cp_msg = salt_client.cp_push( target=ip, source_path=remote_path, upload_path=his_obj.file_name ) if not cp_flag: return change_status(his_obj, False, cp_msg) package_dir = os.path.join(salt_data, f"var/cache/salt/master/minions/{ip}/files/{his_obj.file_name}") size = os.path.getsize(package_dir) / 1024 / 1024 his_obj.file_size = "%.3fM" % size his_obj.remote_path = "" _out, _err, _code = cmd(f"mv {package_dir} {his_obj.retain_path}") if _code == 0: logger.info(f"成功同步到omp {his_obj.file_name}") return change_status(his_obj, True, "success") return change_status(his_obj, False, _err) ================================================ FILE: omp_server/backups/urls.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- """ 备份相关的路由 """ from rest_framework.routers import DefaultRouter from backups.views import BackupSettingView, BackupHistoryView, \ CanBackupInstancesView, BackupCustomView, BackupCustomRepeatView router = DefaultRouter() # 获取可备份实例列表 router.register(r'canBackupInstances', CanBackupInstancesView, basename='canBackupInstances') # 备份设置获取/创建/更新/删除 单次测试 router.register(r'backupSettings', BackupSettingView, basename='backupSettings') # 自定义接口 router.register(r'backupCustom', BackupCustomView, basename='backupCustom') # 自定义接口校验 router.register(r'backupRepeatCustom', BackupCustomRepeatView, basename='backupRepeatCustom') # 备份历史记录、删除备份 router.register(r'backupHistory', BackupHistoryView, basename='backupHistory') urlpatterns = router.urls ================================================ FILE: omp_server/backups/views.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- import logging from rest_framework.mixins import ListModelMixin, CreateModelMixin, DestroyModelMixin, UpdateModelMixin from rest_framework.viewsets import GenericViewSet from rest_framework.response import Response from rest_framework.exceptions import ValidationError from backups.backups_serializers import BackupHistorySerializer, \ BackupHistoryIdsSerializer, BackupSettingSerializer, \ BackupCustomSerializer, BackupCustomRepeatSerializer from backups.backups_utils import rm_backend_file, \ check_ing from utils.plugin.crontab_utils import change_task from backups.tasks import backup_service, pull_back_file from db_models.models import BackupSetting, BackupHistory, Service, \ BackupCustom from utils.common.paginations import PageNumberPager from utils.parse_config import BACKUP_SERVICE logger = logging.getLogger("server") class CanBackupInstancesView(GenericViewSet, ListModelMixin): """ 获取可备份实例列表 """ get_description = "读取可备份实例列表" def list(self, request, *args, **kwargs): # ToDo 找不出更合适的继承 ser_data = Service.objects.filter( service__app_name__in=BACKUP_SERVICE ).filter(service_status=Service.SERVICE_STATUS_NORMAL).values_list("service_instance_name", flat=True) # back_instance = BackupSetting.objects.all().values_list("backup_instances", flat=True) # back_set = [] # for instance in back_instance: # back_set.extend(instance) # ser_data = list(set(ser_data) - set(back_set)) return Response(data=list(ser_data)) class BackupSettingView(GenericViewSet, ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin): """ 操作备份策略 """ get_description = "读取备份策略" post_description = "更新备份策略" serializer_class = BackupSettingSerializer queryset = BackupSetting.objects.all().order_by("-id") def create(self, request, *args, **kwargs): """ 更新备份策略 """ set_id = request.data.pop("id", 0) extend_field = request.data.pop("backup_custom", []) if set_id: # 任务策略制定后触发一次备份任务 if check_ing(BackupSetting.objects.filter(id=set_id).first()): return Response(data={"code": 1, "message": f"当前策略存在正在备份的实例或id{set_id}不存在"}) backup_service.delay(task_id=set_id) return Response("任务下发成功") serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) obj = serializer.save() # 多对多 for field in extend_field: custom = BackupCustom.objects.filter(**field).first() obj.backup_custom.add(custom) code, msg = change_task(obj.id, request.data) return Response(msg) def update(self, request, *args, **kwargs): instance = self.get_object() extend_field = request.data.pop("backup_custom", []) extend_con = [field["id"] for field in extend_field] instance.backup_custom.set(extend_con) instance.save() super(BackupSettingView, self).update( request, request, *args, **kwargs) code, msg = change_task(instance.id, request.data) return Response(msg) def destroy(self, request, *args, **kwargs): instance = self.get_object() super(BackupSettingView, self).destroy( request, request, *args, **kwargs) code, msg = change_task(instance.id) return Response(msg) class BackupHistoryView(GenericViewSet, ListModelMixin, DestroyModelMixin, UpdateModelMixin ): """ 备份记录相关视图 """ queryset = BackupHistory.objects.all().order_by("-create_time") pagination_class = PageNumberPager # 关闭权限、认证设置 authentication_classes = () permission_classes = () delete_description = "删除备份记录文件" get_description = "获取备份历史记录" def get_serializer_class(self): if self.request is not None and self.request.method == "POST": return BackupHistoryIdsSerializer return BackupHistorySerializer def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) instance = serializer.validated_data["id"] fail_files = rm_backend_file(instance) if fail_files: return Response(data={"code": 0, "message": f"删除{','.join(fail_files)}可能已不存在!"}) return Response("删除记录成功") def update(self, request, *args, **kwargs): # 转换code意义,考虑是否把含义变更 code = 0 if request.data.pop("result") != "0" else 1 remote_path = request.data.get("remote_path", "") need_push = request.data.get("need_push", False) # 文件备份 request.data["file_deleted"] = False if remote_path else True # 成功 且 存在要拉取的文件 if code and remote_path and need_push: if remote_path.startswith("/") and remote_path.endswith(".tar.gz"): instance = self.get_object() pull_back_file.delay( instance.id, remote_path, request.data["ip"]) else: raise ValidationError(f"文件不合法{remote_path}") request.data["remote_path"] = "" else: request.data["retain_path"] = "" request.data["result"] = code return super(BackupHistoryView, self).update( request, request, *args, **kwargs) class BackupCustomView(GenericViewSet, ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin): """ 备份自定义字典视图 """ serializer_class = BackupCustomSerializer queryset = BackupCustom.objects.all() delete_description = "删除备份自定义字典" get_description = "获取备份自定义字典" put_description = "修改备份自定义字典" post_description = "创建备份自定义字典" def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) BackupCustom.objects.create(**serializer.data) return Response("创建成功") class BackupCustomRepeatView(GenericViewSet, ListModelMixin): """ list: 查询备份字典是否有多对多关联 """ serializer_class = BackupCustomRepeatSerializer get_description = "查询备份字典是否有多对多关联" def get_queryset(self): bk_id = self.request.query_params.get('id') if bk_id: return BackupCustom.objects.filter(id=bk_id) else: raise ValidationError("需要填写id") ================================================ FILE: omp_server/db_models/__init__.py ================================================ default_app_config = "db_models.apps.DbModelsConfig" ================================================ FILE: omp_server/db_models/admin.py ================================================ # from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/db_models/apps.py ================================================ from django.apps import AppConfig class DbModelsConfig(AppConfig): name = 'db_models' def ready(self): # signals are imported, so that they are defined and can be used from db_models import receivers ================================================ FILE: omp_server/db_models/migrations/0001_initial.py ================================================ # Generated by Django 3.1.4 on 2021-12-01 09:44 import django.contrib.auth.models import django.contrib.auth.validators from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): initial = True dependencies = [ ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ migrations.CreateModel( name='AlertSendWaySetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('used', models.BooleanField(default=False, verbose_name='是否启用')), ('env_id', models.IntegerField(default=0, verbose_name='环境id')), ('way_name', models.CharField(max_length=64, verbose_name='告警推送服务名称')), ('server_url', models.TextField(default='', verbose_name='告警推送服务url')), ('way_token', models.CharField(default='', max_length=255, verbose_name='服务token')), ('extra_info', models.JSONField(default=dict, verbose_name='服务其他信息')), ], options={ 'verbose_name': '告警推送通道设置', 'verbose_name_plural': '告警推送通道设置', 'db_table': 'omp_alert_send_way_setting', }, ), migrations.CreateModel( name='ApplicationHub', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('is_release', models.BooleanField(default=False, help_text='是否发布', verbose_name='是否发布')), ('app_type', models.IntegerField(choices=[(0, '组件'), (1, '服务')], default=0, help_text='应用类型', verbose_name='应用类型')), ('app_name', models.CharField(help_text='应用名称', max_length=256, verbose_name='应用名称')), ('app_version', models.CharField(help_text='应用版本', max_length=256, verbose_name='应用版本')), ('app_description', models.CharField(blank=True, help_text='应用描述', max_length=2048, null=True, verbose_name='应用描述')), ('app_port', models.TextField(blank=True, help_text='应用端口', null=True, verbose_name='应用端口')), ('app_dependence', models.TextField(blank=True, help_text='应用依赖', null=True, verbose_name='应用依赖')), ('app_install_args', models.TextField(blank=True, help_text='安装参数', null=True, verbose_name='安装参数')), ('app_controllers', models.TextField(blank=True, help_text='应用控制脚本', null=True, verbose_name='应用控制脚本')), ('app_logo', models.TextField(blank=True, help_text='应用图标', null=True, verbose_name='应用图标')), ('extend_fields', models.JSONField(blank=True, help_text='冗余字段', null=True, verbose_name='冗余字段')), ('is_base_env', models.BooleanField(default=False, help_text='是否为基础环境', verbose_name='是否为基础环境')), ('app_monitor', models.JSONField(blank=True, help_text='监控使用字段', null=True, verbose_name='监控使用字段')), ], options={ 'verbose_name': '应用商店服务', 'verbose_name_plural': '应用商店服务', 'db_table': 'omp_application', }, ), migrations.CreateModel( name='ClusterInfo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('is_deleted', models.BooleanField(default=False, help_text='软删除')), ('cluster_service_name', models.CharField(blank=True, help_text='集群所属服务', max_length=36, null=True, verbose_name='集群所属服务')), ('cluster_type', models.CharField(blank=True, help_text='集群类型', max_length=36, null=True, verbose_name='集群类型')), ('cluster_name', models.CharField(help_text='集群名称', max_length=64, verbose_name='集群名称')), ('connect_info', models.CharField(blank=True, help_text='集群连接信息', max_length=512, null=True, verbose_name='集群连接信息')), ('connect_info_parse_type', models.CharField(blank=True, help_text='连接信息解析方式', max_length=32, null=True, verbose_name='连接信息解析方式')), ], options={ 'verbose_name': '集群信息表', 'verbose_name_plural': '集群信息表', 'db_table': 'omp_cluster_info', }, ), migrations.CreateModel( name='EmailSMTPSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('email_host', models.EmailField(max_length=254, null=True, verbose_name='邮箱SMTP主机地址:smtp.163.com')), ('email_port', models.IntegerField(null=True, verbose_name='邮箱SMTP端口:465')), ('email_host_user', models.CharField(max_length=128, null=True, verbose_name='邮箱SMTP服务器用户名:a@163.com')), ('email_host_password', models.CharField(max_length=128, null=True, verbose_name='邮箱SMTP服务器秘钥')), ], options={ 'verbose_name': '平台邮件服务器配置', 'verbose_name_plural': '平台邮件服务器配置', 'db_table': 'omp_email_smtp_setting', }, ), migrations.CreateModel( name='Env', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='环境名称', max_length=256, verbose_name='环境名称')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ], options={ 'verbose_name': '环境', 'verbose_name_plural': '环境', 'db_table': 'omp_env', }, ), migrations.CreateModel( name='GrafanaMainPage', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('instance_name', models.CharField(help_text='信息面板实例名字', max_length=32, unique=True, verbose_name='实例名字')), ('instance_url', models.CharField(help_text='实例文根地址', max_length=255, unique=True, verbose_name='实例地址')), ], options={ 'verbose_name': 'grafana面板记录', 'verbose_name_plural': 'grafana面板记录', 'db_table': 'omp_grafana_url', }, ), migrations.CreateModel( name='Host', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('is_deleted', models.BooleanField(default=False, help_text='软删除')), ('instance_name', models.CharField(help_text='实例名', max_length=64, verbose_name='实例名')), ('ip', models.GenericIPAddressField(help_text='IP地址', verbose_name='IP地址')), ('port', models.IntegerField(default=22, help_text='SSH端口', verbose_name='SSH端口')), ('username', models.CharField(help_text='SSH登录用户名', max_length=256, verbose_name='SSH登录用户名')), ('password', models.CharField(help_text='SSH登录密码', max_length=256, verbose_name='SSH登录密码')), ('data_folder', models.CharField(default='/data', help_text='数据分区', max_length=256, verbose_name='数据分区')), ('service_num', models.IntegerField(default=0, help_text='服务个数', verbose_name='服务个数')), ('alert_num', models.IntegerField(default=0, help_text='告警次数', verbose_name='告警次数')), ('operate_system', models.CharField(help_text='操作系统', max_length=128, verbose_name='操作系统')), ('host_name', models.CharField(blank=True, help_text='主机名', max_length=64, null=True, verbose_name='主机名')), ('memory', models.IntegerField(blank=True, help_text='内存', null=True, verbose_name='内存')), ('cpu', models.IntegerField(blank=True, help_text='CPU', null=True, verbose_name='CPU')), ('disk', models.JSONField(blank=True, help_text='磁盘', null=True, verbose_name='磁盘')), ('host_agent', models.CharField(choices=[(0, '正常'), (1, '重启中'), (2, '启动失败'), (3, '部署中'), (4, '部署失败')], default=3, help_text='主机Agent状态', max_length=16, verbose_name='主机Agent状态')), ('monitor_agent', models.CharField(choices=[(0, '正常'), (1, '重启中'), (2, '启动失败'), (3, '部署中'), (4, '部署失败')], default=3, help_text='监控Agent状态', max_length=16, verbose_name='监控Agent状态')), ('host_agent_error', models.CharField(blank=True, help_text='主机Agent异常信息', max_length=256, null=True, verbose_name='主机Agent异常信息')), ('monitor_agent_error', models.CharField(blank=True, help_text='监控Agent异常信息', max_length=256, null=True, verbose_name='监控Agent异常信息')), ('is_maintenance', models.BooleanField(default=False, help_text='维护模式', verbose_name='维护模式')), ('agent_dir', models.CharField(default='/data', help_text='Agent安装目录', max_length=256, verbose_name='Agent安装目录')), ('env', models.ForeignKey(help_text='环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='环境')), ], options={ 'verbose_name': '主机', 'verbose_name_plural': '主机', 'db_table': 'omp_host', 'ordering': ('-created',), }, ), migrations.CreateModel( name='HostThreshold', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('index_type', models.CharField(help_text='指标项名称', max_length=64)), ('condition', models.CharField(help_text='阈值条件', max_length=32)), ('condition_value', models.CharField(help_text='阈值数值,百分号格式', max_length=128)), ('alert_level', models.CharField(help_text='告警级别', max_length=32)), ('create_date', models.DateTimeField(auto_now_add=True)), ('env_id', models.IntegerField(default=1, help_text='环境id')), ], options={ 'db_table': 'omp_host_threshold', }, ), migrations.CreateModel( name='InspectionHistory', fields=[ ('id', models.AutoField(help_text='自增主键', primary_key=True, serialize=False, unique=True)), ('inspection_name', models.CharField(help_text='报告名称:巡检类型名称', max_length=256)), ('inspection_type', models.CharField(default='service', help_text='巡检类型,service、host、deep', max_length=32)), ('inspection_status', models.IntegerField(default=0, help_text='巡检状态:1-进行中;2-成功;3-失败')), ('execute_type', models.CharField(default='man', help_text='执行方式: 手动-man;定时:auto', max_length=32)), ('inspection_operator', models.CharField(help_text='操作人员-当前登录人', max_length=16)), ('hosts', models.JSONField(blank=True, help_text="巡检主机:['10.0.9.158']", null=True)), ('services', models.JSONField(blank=True, help_text='巡检组件: [8,9]', null=True)), ('start_time', models.DateTimeField(auto_now_add=True, help_text='开始时间')), ('end_time', models.DateTimeField(help_text='结束时间,后端后补', null=True)), ('duration', models.IntegerField(default=0, help_text='巡检用时:单位s,后端后补')), ('send_email_result', models.IntegerField(choices=[('发送成功', 1), ('发送中', 2), ('发送失败', 0), ('未发送', 3)], default=3, verbose_name='邮件推送状态')), ('env', models.ForeignKey(help_text='当前环境id', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='当前环境id')), ], options={ 'verbose_name': '巡检记录历史表', 'verbose_name_plural': '巡检记录历史表', 'db_table': 'inspection_history', 'ordering': ('-start_time',), }, ), migrations.CreateModel( name='Labels', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('label_name', models.CharField(help_text='标签名称', max_length=16, verbose_name='标签名称')), ('label_type', models.IntegerField(choices=[(0, '组件'), (1, '应用')], default=0, help_text='标签类型', verbose_name='标签类型')), ], options={ 'verbose_name': '应用产品标签表', 'verbose_name_plural': '应用产品标签表', 'db_table': 'omp_labels', }, ), migrations.CreateModel( name='MainInstallHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('operator', models.CharField(default='admin', help_text='用户', max_length=32, verbose_name='操作用户')), ('operation_uuid', models.CharField(help_text='部署操作uuid', max_length=36, verbose_name='部署操作uuid')), ('task_id', models.CharField(blank=True, help_text='异步任务id', max_length=36, null=True, verbose_name='异步任务id')), ('install_status', models.IntegerField(choices=[(0, '待安装'), (1, '安装中'), (2, '安装成功'), (3, '安装失败')], default=0, help_text='安装状态', verbose_name='安装状态')), ('install_args', models.JSONField(blank=True, help_text='主表安装信息', null=True, verbose_name='主表安装信息')), ('install_log', models.TextField(help_text='MAIN安装日志', verbose_name='MAIN安装日志')), ], options={ 'verbose_name': '主安装记录表', 'verbose_name_plural': '主安装记录表', 'db_table': 'omp_main_install_history', }, ), migrations.CreateModel( name='Maintain', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('matcher_name', models.CharField(help_text='匹配标签', max_length=1024, verbose_name='匹配标签')), ('matcher_value', models.CharField(help_text='匹配值', max_length=1024, verbose_name='匹配值')), ('maintain_id', models.CharField(help_text='维护唯一标识', max_length=1024, verbose_name='维护唯一标识')), ], options={ 'verbose_name': '维护记录', 'verbose_name_plural': '维护记录', 'db_table': 'omp_maintain', }, ), migrations.CreateModel( name='ModuleSendEmailSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('module', models.CharField(max_length=64, verbose_name='功能模块:BackupSetting,JobSetting')), ('send_email', models.BooleanField(default=False, verbose_name='是否开启邮件推送')), ('to_users', models.TextField(default='', verbose_name='邮箱接收用户')), ('env_id', models.IntegerField(default=1, verbose_name='环境id')), ], options={ 'verbose_name': '平台邮件发送账号配置', 'verbose_name_plural': '平台邮件发送账号配置', 'db_table': 'omp_module_email_send_setting', }, ), migrations.CreateModel( name='MonitorUrl', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text='监控类别', max_length=32, unique=True, verbose_name='监控类别')), ('monitor_url', models.CharField(help_text='请求地址', max_length=128, verbose_name='请求地址')), ], options={ 'verbose_name': '监控地址记录', 'verbose_name_plural': '监控地址记录', 'db_table': 'omp_promemonitor_url', }, ), migrations.CreateModel( name='OperateLog', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(help_text='操作用户', max_length=128, verbose_name='操作用户')), ('request_method', models.CharField(help_text='请求方法', max_length=32, verbose_name='请求方法')), ('request_ip', models.GenericIPAddressField(blank=True, help_text='请求源IP', null=True, verbose_name='请求源IP')), ('request_url', models.CharField(help_text='用户目标URL', max_length=256, verbose_name='用户目标URL')), ('description', models.CharField(help_text='用户行为描述', max_length=256, verbose_name='用户行为描述')), ('response_code', models.IntegerField(default=0, help_text='响应状态码', verbose_name='响应状态码')), ('request_result', models.TextField(default='success', help_text='请求结果', verbose_name='请求结果')), ('create_time', models.DateTimeField(auto_now_add=True, help_text='操作发生时间', verbose_name='操作发生时间')), ], options={ 'verbose_name': '用户操作记录', 'verbose_name_plural': '用户操作记录', 'db_table': 'omp_user_operate_log', }, ), migrations.CreateModel( name='Service', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('ip', models.GenericIPAddressField(help_text='服务所在主机', verbose_name='服务所在主机')), ('service_instance_name', models.CharField(help_text='服务实例名称', max_length=64, verbose_name='服务实例名称')), ('service_port', models.TextField(blank=True, help_text='服务端口', null=True, verbose_name='服务端口')), ('service_controllers', models.JSONField(blank=True, help_text='服务控制脚本', null=True, verbose_name='服务控制脚本')), ('service_role', models.CharField(blank=True, help_text='服务角色', max_length=128, null=True, verbose_name='服务角色')), ('service_status', models.IntegerField(choices=[(0, '正常'), (1, '启动中'), (2, '停止中'), (3, '重启中'), (4, '停止'), (5, '未知'), (6, '安装中'), (7, '安装失败')], default=5, help_text='服务状态', verbose_name='服务状态')), ('alert_count', models.IntegerField(default=0, help_text='告警次数', verbose_name='告警次数')), ('self_healing_count', models.IntegerField(default=0, help_text='服务自愈次数', verbose_name='服务自愈次数')), ('service_dependence', models.TextField(blank=True, help_text='服务依赖关系', null=True, verbose_name='服务依赖关系')), ('cluster', models.ForeignKey(blank=True, help_text='所属集群', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.clusterinfo')), ('env', models.ForeignKey(blank=True, help_text='所属环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env')), ('service', models.ForeignKey(blank=True, help_text='服务表外键', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.applicationhub')), ], options={ 'verbose_name': '服务实例表', 'verbose_name_plural': '服务实例表', 'db_table': 'omp_service', 'ordering': ('-created',), }, ), migrations.CreateModel( name='ServiceConnectInfo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('service_name', models.CharField(help_text='服务名', max_length=32, verbose_name='服务名')), ('service_username', models.CharField(blank=True, help_text='用户名', max_length=512, null=True, verbose_name='用户名')), ('service_password', models.CharField(blank=True, help_text='密码', max_length=512, null=True, verbose_name='密码')), ('service_username_enc', models.CharField(blank=True, help_text='加密用户名', max_length=512, null=True, verbose_name='加密用户名')), ('service_password_enc', models.CharField(blank=True, help_text='加密密码', max_length=512, null=True, verbose_name='加密密码')), ], options={ 'verbose_name': '用户名密码信息表', 'verbose_name_plural': '用户名密码信息表', 'db_table': 'omp_service_connect_info', }, ), migrations.CreateModel( name='ServiceCustomThreshold', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('service_name', models.CharField(max_length=64, verbose_name='服务名称')), ('index_type', models.CharField(max_length=64, verbose_name='指标项名称')), ('condition', models.CharField(max_length=32, verbose_name='阈值条件')), ('condition_value', models.CharField(max_length=128, verbose_name='阈值数值')), ('alert_level', models.CharField(max_length=32, verbose_name='告警级别')), ('create_date', models.DateTimeField(auto_now_add=True)), ('env_id', models.IntegerField(default=1, verbose_name='环境id')), ], options={ 'verbose_name': '服务定制指标阈值设置', 'verbose_name_plural': '服务定制指标阈值设置', 'db_table': 'omp_service_custom_threshold', }, ), migrations.CreateModel( name='ServiceThreshold', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('index_type', models.CharField(max_length=64, verbose_name='指标项名称')), ('condition', models.CharField(max_length=32, verbose_name='阈值条件')), ('condition_value', models.CharField(max_length=128, verbose_name='阈值数值,百分号格式')), ('alert_level', models.CharField(max_length=32, verbose_name='告警级别')), ('create_date', models.DateTimeField(auto_now_add=True)), ('env_id', models.IntegerField(default=1, verbose_name='环境id')), ], options={ 'db_table': 'omp_service_threshold', }, ), migrations.CreateModel( name='UploadPackageHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('is_deleted', models.BooleanField(default=False, help_text='软删除')), ('operation_uuid', models.CharField(help_text='唯一操作uuid', max_length=64, verbose_name='唯一操作uuid')), ('operation_user', models.CharField(blank=True, help_text='操作用户', max_length=64, null=True, verbose_name='操作用户')), ('package_name', models.CharField(help_text='安装包名称', max_length=256, verbose_name='安装包名称')), ('package_md5', models.CharField(help_text='安装包MD5值', max_length=64, verbose_name='安装包MD5值')), ('package_path', models.CharField(help_text='安装包路径', max_length=512, verbose_name='安装包路径')), ('package_status', models.IntegerField(choices=[(0, '成功'), (1, '失败'), (2, '解析中'), (3, '发布成功'), (4, '发布失败'), (5, '发布中')], default=2, help_text='安装包状态', verbose_name='安装包状态')), ('error_msg', models.CharField(blank=True, help_text='错误消息', max_length=1024, null=True, verbose_name='错误消息')), ('package_parent', models.ForeignKey(blank=True, help_text='父级包', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.uploadpackagehistory')), ], options={ 'verbose_name': '上传安装包记录', 'verbose_name_plural': '上传安装包记录', 'db_table': 'omp_upload_package_history', }, ), migrations.CreateModel( name='ServiceHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(help_text='操作用户', max_length=128, verbose_name='操作用户')), ('description', models.CharField(help_text='用户行为描述', max_length=1024, verbose_name='用户行为描述')), ('result', models.CharField(default='success', help_text='操作结果', max_length=1024, verbose_name='操作结果')), ('created', models.DateTimeField(auto_now_add=True, help_text='发生时间', null=True, verbose_name='发生时间')), ('service', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.service', verbose_name='服务')), ], options={ 'verbose_name': '服务操作记录', 'verbose_name_plural': '服务操作记录', 'db_table': 'omp_service_operate_log', 'ordering': ('-created',), }, ), migrations.AddField( model_name='service', name='service_connect_info', field=models.ForeignKey(blank=True, help_text='用户名密码信息', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.serviceconnectinfo'), ), migrations.CreateModel( name='ProductHub', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('is_release', models.BooleanField(default=False, help_text='是否发布', verbose_name='是否发布')), ('pro_name', models.CharField(help_text='产品名称', max_length=256, verbose_name='产品名称')), ('pro_version', models.CharField(help_text='产品版本', max_length=256, verbose_name='产品版本')), ('pro_description', models.CharField(blank=True, help_text='产品描述', max_length=2048, null=True, verbose_name='产品描述')), ('pro_dependence', models.TextField(blank=True, help_text='产品依赖', null=True, verbose_name='产品依赖')), ('pro_services', models.TextField(blank=True, help_text='服务列表', null=True, verbose_name='服务列表')), ('pro_logo', models.TextField(blank=True, help_text='产品图标', null=True, verbose_name='产品图标')), ('extend_fields', models.JSONField(blank=True, help_text='冗余字段', null=True, verbose_name='冗余字段')), ('pro_labels', models.ManyToManyField(help_text='所属标签', to='db_models.Labels')), ('pro_package', models.ForeignKey(blank=True, help_text='安装包', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.uploadpackagehistory', verbose_name='安装包')), ], options={ 'verbose_name': '应用商店产品', 'verbose_name_plural': '应用商店产品', 'db_table': 'omp_product', }, ), migrations.CreateModel( name='Product', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('product_instance_name', models.CharField(blank=True, help_text='安装产品时输入的实例名称', max_length=64, null=True, verbose_name='产品实例名称')), ('product', models.ForeignKey(blank=True, help_text='所属产品', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.producthub')), ], options={ 'verbose_name': '产品实例表', 'verbose_name_plural': '产品实例表', 'db_table': 'omp_product_instance', 'ordering': ('-created',), }, ), migrations.CreateModel( name='InspectionReport', fields=[ ('id', models.AutoField(help_text='自增主键', primary_key=True, serialize=False, unique=True)), ('file_name', models.CharField(blank=True, help_text='导出文件名', max_length=128, null=True)), ('scan_info', models.JSONField(blank=True, help_text='扫描统计', null=True)), ('scan_result', models.JSONField(blank=True, help_text='分析结果', null=True)), ('risk_data', models.JSONField(blank=True, help_text='风险指标', null=True)), ('host_data', models.JSONField(blank=True, help_text='主机列表', null=True)), ('serv_plan', models.JSONField(blank=True, help_text='服务平面图', null=True)), ('serv_data', models.JSONField(blank=True, help_text='服务列表', null=True)), ('inst_id', models.OneToOneField(help_text='巡检记录历史表id', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.inspectionhistory', verbose_name='巡检记录历史表')), ], options={ 'verbose_name': '巡检任务 报告数据', 'verbose_name_plural': '巡检任务 报告数据', 'db_table': 'inspection_report', 'ordering': ('id',), }, ), migrations.CreateModel( name='InspectionCrontab', fields=[ ('id', models.AutoField(help_text='自增主键', primary_key=True, serialize=False, unique=True)), ('job_type', models.IntegerField(choices=[(0, '深度分析'), (1, '主机巡检'), (2, '组件巡检')], default=0, help_text='任务类型:0-深度分析 1-主机巡检 2-组建巡检')), ('job_name', models.CharField(help_text='任务名称', max_length=128)), ('is_start_crontab', models.IntegerField(default=0, help_text='是否开启定时任务:0-开启,1-关闭')), ('crontab_detail', models.JSONField(help_text='定时任务详情')), ('create_date', models.DateTimeField(auto_now_add=True, help_text='创建时间')), ('update_time', models.DateTimeField(auto_now=True, help_text='修改时间')), ('env', models.ForeignKey(help_text='环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='环境')), ], options={ 'verbose_name': '巡检任务 定时配置表', 'verbose_name_plural': '巡检任务 定时配置表', 'db_table': 'inspection_crontab', 'ordering': ('id',), }, ), migrations.CreateModel( name='HostOperateLog', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(help_text='操作用户', max_length=128, verbose_name='操作用户')), ('description', models.CharField(help_text='用户行为描述', max_length=1024, verbose_name='用户行为描述')), ('result', models.CharField(default='success', help_text='操作结果', max_length=1024, verbose_name='操作结果')), ('created', models.DateTimeField(auto_now_add=True, help_text='发生时间', null=True, verbose_name='发生时间')), ('host', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.host', verbose_name='主机')), ], options={ 'verbose_name': '主机操作记录', 'verbose_name_plural': '主机操作记录', 'db_table': 'omp_host_operate_log', 'ordering': ('-created',), }, ), migrations.CreateModel( name='DetailInstallHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('install_step_status', models.IntegerField(choices=[(0, '待安装'), (1, '安装中'), (2, '安装成功'), (3, '安装失败')], default=0, help_text='安装步骤状态', verbose_name='安装步骤状态')), ('send_flag', models.IntegerField(default=0, help_text='0-待发送 1-发送中 2-发送成功 3-发送失败', verbose_name='发包状态')), ('send_msg', models.TextField(help_text='发包日志', verbose_name='发包日志')), ('unzip_flag', models.IntegerField(default=0, help_text='0-待解压 1-解压中 2-解压成功 3-解压失败', verbose_name='解压包状态')), ('unzip_msg', models.TextField(help_text='解压日志', verbose_name='解压日志')), ('install_flag', models.IntegerField(default=0, help_text='0-待安装 1-安装中 2-安装成功 3-安装失败', verbose_name='安装执行状态')), ('install_msg', models.TextField(help_text='安装日志', verbose_name='安装日志')), ('init_flag', models.IntegerField(default=0, help_text='0-待初始化 1-初始化中 2-初始化成功 3-初始化失败', verbose_name='初始化执行状态')), ('init_msg', models.TextField(help_text='初始化日志', verbose_name='初始化日志')), ('start_flag', models.IntegerField(default=0, help_text='0-待启动 1-启动中 2-启动成功 3-启动失败', verbose_name='启动执行状态')), ('start_msg', models.TextField(help_text='启动日志', verbose_name='启动日志')), ('install_detail_args', models.JSONField(blank=True, help_text='详情表安装信息', null=True, verbose_name='详情表安装信息')), ('post_action_flag', models.IntegerField(default=0, help_text='0-待执行 1-执行中 2-执行成功 3-执行失败 4-无需执行', verbose_name='安装后执行动作标记')), ('post_action_msg', models.TextField(default='', help_text='安装后执行动作日志', verbose_name='安装后执行动作日志')), ('main_install_history', models.ForeignKey(blank=True, help_text='关联主安装记录', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.maininstallhistory')), ('service', models.ForeignKey(blank=True, help_text='关联服务对象', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.service')), ], options={ 'verbose_name': '安装记录详情表', 'verbose_name_plural': '安装记录详情表', 'db_table': 'omp_detail_install_history', }, ), migrations.AddField( model_name='clusterinfo', name='service_connect_info', field=models.ForeignKey(blank=True, help_text='用户名密码信息', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.serviceconnectinfo'), ), migrations.AddField( model_name='applicationhub', name='app_labels', field=models.ManyToManyField(help_text='所属标签', to='db_models.Labels'), ), migrations.AddField( model_name='applicationhub', name='app_package', field=models.ForeignKey(blank=True, help_text='安装包', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.uploadpackagehistory', verbose_name='安装包'), ), migrations.AddField( model_name='applicationhub', name='product', field=models.ForeignKey(blank=True, help_text='所属产品', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.producthub', verbose_name='所属产品'), ), migrations.CreateModel( name='Alert', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('is_read', models.IntegerField(default=0, help_text='此消息是否已读,0-未读;1-已读', verbose_name='已读')), ('alert_type', models.CharField(default='', help_text='告警类型,主机host,服务service', max_length=32, verbose_name='告警类型')), ('alert_host_ip', models.CharField(default='', help_text='告警来源主机ip', max_length=64, verbose_name='告警主机ip')), ('alert_service_name', models.CharField(default='', help_text='服务类告警中的服务名称', max_length=128, verbose_name='告警服务名称')), ('alert_instance_name', models.CharField(default='', help_text='告警实例名称', max_length=128, verbose_name='告警实例名称')), ('alert_service_type', models.CharField(default='', help_text='服务所属类型', max_length=128, verbose_name='告警服务类型')), ('alert_level', models.CharField(default='', help_text='告警级别', max_length=1024, verbose_name='告警级别')), ('alert_describe', models.CharField(default='', help_text='告警描述', max_length=1024, verbose_name='告警描述')), ('alert_receiver', models.CharField(default='', help_text='告警接收人', max_length=256, verbose_name='告警接收人')), ('alert_resolve', models.CharField(default='', help_text='告警解决方案', max_length=1024, verbose_name='告警解决方案')), ('alert_time', models.DateTimeField(help_text='告警发生时间', verbose_name='告警发生时间')), ('create_time', models.DateTimeField(auto_now_add=True, help_text='告警信息入库时间', verbose_name='告警信息入库时间')), ('monitor_path', models.CharField(blank=True, help_text='跳转grafana路由', max_length=2048, null=True, verbose_name='跳转监控路径')), ('monitor_log', models.CharField(blank=True, help_text='跳转grafana日志页面路由', max_length=2048, null=True, verbose_name='跳转监控日志路径')), ('fingerprint', models.CharField(blank=True, help_text='告警的唯一标识', max_length=1024, null=True, verbose_name='告警的唯一标识')), ('env', models.ForeignKey(help_text='环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='环境')), ], options={ 'verbose_name': '告警记录', 'verbose_name_plural': '告警记录', 'db_table': 'omp_alert', }, ), migrations.CreateModel( name='UserProfile', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('password', models.CharField(max_length=128, verbose_name='password')), ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')), ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')), ], options={ 'verbose_name': '用户', 'verbose_name_plural': '用户', 'db_table': 'omp_user_profile', }, managers=[ ('objects', django.contrib.auth.models.UserManager()), ], ), migrations.AlterUniqueTogether( name='applicationhub', unique_together={('app_name', 'app_version')}, ), ] ================================================ FILE: omp_server/db_models/migrations/0002_auto_20211202_1830.py ================================================ # Generated by Django 3.1.4 on 2021-12-02 18:30 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0001_initial'), ] operations = [ migrations.AlterField( model_name='service', name='service_status', field=models.IntegerField(choices=[(0, '正常'), (1, '启动中'), (2, '停止中'), (3, '重启中'), (4, '停止'), (5, '未知'), (6, '安装中'), (7, '安装失败'), (8, '待安装')], default=5, help_text='服务状态', verbose_name='服务状态'), ), ] ================================================ FILE: omp_server/db_models/migrations/0003_host_init_status.py ================================================ # Generated by Django 3.1.4 on 2021-12-02 19:38 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0002_auto_20211202_1830'), ] operations = [ migrations.AddField( model_name='host', name='init_status', field=models.CharField(choices=[(0, '未执行'), (1, '成功'), (2, '失败')], default=0, help_text='主机初始化状态', max_length=16, verbose_name='主机初始化状态'), ), ] ================================================ FILE: omp_server/db_models/migrations/0004_auto_20211203_1617.py ================================================ # Generated by Django 3.1.4 on 2021-12-03 16:17 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('db_models', '0003_host_init_status'), ] operations = [ migrations.AlterField( model_name='maininstallhistory', name='install_status', field=models.IntegerField(choices=[(0, '待安装'), (1, '安装中'), (2, '安装成功'), (3, '安装失败'), (4, '正在注册')], default=0, help_text='安装状态', verbose_name='安装状态'), ), migrations.CreateModel( name='PreInstallHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('name', models.CharField(default='preInstall', help_text='名称', max_length=32, verbose_name='名称')), ('ip', models.GenericIPAddressField(help_text='主机ip地址', verbose_name='主机ip地址')), ('install_flag', models.IntegerField(default=0, help_text='0-待安装 1-安装中 2-安装成功 3-安装失败', verbose_name='安装标志')), ('install_log', models.TextField(help_text='主机层安装日志', verbose_name='主机层安装日志')), ('main_install_history', models.ForeignKey(blank=True, help_text='关联主安装记录', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.maininstallhistory')), ], options={ 'verbose_name': '前置安装记录', 'verbose_name_plural': '前置安装记录', 'db_table': 'omp_pre_install_history', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0005_auto_20211206_1723.py ================================================ # Generated by Django 3.1.4 on 2021-12-06 17:23 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('db_models', '0004_auto_20211203_1617'), ] operations = [ migrations.AlterField( model_name='preinstallhistory', name='name', field=models.CharField(default='初始化安装流程', help_text='名称', max_length=32, verbose_name='名称'), ), migrations.AlterField( model_name='service', name='service_status', field=models.IntegerField(choices=[(0, '正常'), (1, '启动中'), (2, '停止中'), (3, '重启中'), (4, '停止'), (5, '未知'), (6, '安装中'), (7, '安装失败'), (8, '待安装'), (9, '删除中')], default=5, help_text='服务状态', verbose_name='服务状态'), ), migrations.CreateModel( name='PostInstallHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('name', models.CharField(default='安装后续任务', help_text='名称', max_length=32, verbose_name='名称')), ('ip', models.CharField(default='postAction', help_text='主机ip地址', max_length=128, verbose_name='fake主机ip地址')), ('install_flag', models.IntegerField(default=0, help_text='0-待执行 1-执行中 2-执行成功 3-执行失败', verbose_name='安装标志')), ('install_log', models.TextField(help_text='安装后续任务日志', verbose_name='安装后续任务日志')), ('main_install_history', models.ForeignKey(blank=True, help_text='关联主安装记录', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.maininstallhistory')), ], options={ 'verbose_name': '后置安装记录', 'verbose_name_plural': '后置安装记录', 'db_table': 'omp_post_install_history', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0005_update_init_status.py ================================================ # Generated by Django 3.1.4 on 2021-12-03 14:30 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0004_auto_20211203_1617'), ] operations = [ migrations.AlterField( model_name='host', name='init_status', field=models.CharField(choices=[(0, '成功'), (1, '未执行'), (2, '执行中'), (3, '失败')], default=1, help_text='主机初始化状态', max_length=16, verbose_name='主机初始化状态'), ), ] ================================================ FILE: omp_server/db_models/migrations/0006_merge_20211206_1833.py ================================================ # Generated by Django 3.1.4 on 2021-12-06 18:33 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('db_models', '0005_update_init_status'), ('db_models', '0005_auto_20211206_1723'), ] operations = [ ] ================================================ FILE: omp_server/db_models/migrations/0007_deploymentplan.py ================================================ # Generated by Django 3.1.4 on 2021-12-13 17:14 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0006_merge_20211206_1833'), ] operations = [ migrations.CreateModel( name='DeploymentPlan', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('plan_name', models.CharField(help_text='部署计划名称', max_length=32, verbose_name='部署计划名称')), ('host_num', models.IntegerField(default=0, help_text='主机数量', verbose_name='主机数量')), ('product_num', models.IntegerField(default=0, help_text='产品数量', verbose_name='产品数量')), ('service_num', models.IntegerField(default=0, help_text='服务数量', verbose_name='服务数量')), ('create_user', models.CharField(help_text='创建用户', max_length=16, verbose_name='创建用户')), ('operation_uuid', models.CharField(help_text='部署操作uuid', max_length=36, verbose_name='部署操作uuid')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ], options={ 'verbose_name': '部署计划', 'verbose_name_plural': '部署计划', 'db_table': 'omp_deployment_plan', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0008_service_vip.py ================================================ # Generated by Django 3.1.4 on 2021-12-22 10:20 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0007_deploymentplan'), ] operations = [ migrations.AddField( model_name='service', name='vip', field=models.GenericIPAddressField(blank=True, default=None, help_text='vip地址', null=True, verbose_name='vip地址'), ), ] ================================================ FILE: omp_server/db_models/migrations/0009_auto_20211228_1603.py ================================================ # Generated by Django 3.1.4 on 2021-12-28 16:03 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0008_service_vip'), ] operations = [ migrations.AlterField( model_name='host', name='instance_name', field=models.CharField(help_text='实例名', max_length=64, unique=True, verbose_name='实例名'), ), migrations.AlterField( model_name='host', name='ip', field=models.GenericIPAddressField(help_text='IP地址', unique=True, verbose_name='IP地址'), ), ] ================================================ FILE: omp_server/db_models/migrations/0010_auto_20220114_1830.py ================================================ # Generated by Django 3.1.4 on 2022-01-14 18:30 from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('db_models', '0009_auto_20211228_1603'), ] operations = [ migrations.AlterField( model_name='service', name='service_status', field=models.IntegerField(choices=[(0, '正常'), (1, '启动中'), (2, '停止中'), (3, '重启中'), (4, '停止'), (5, '未知'), (6, '安装中'), (7, '安装失败'), (8, '待安装'), (9, '删除中'), (10, '升级中'), (11, '回滚中')], default=5, help_text='服务状态', verbose_name='服务状态'), ), migrations.CreateModel( name='UpgradeHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('upgrade_state', models.IntegerField(choices=[(0, '等待升级'), (1, '正在升级'), (2, '升级成功'), (3, '升级失败')], default=0, verbose_name='升级结果')), ('env', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='所属环境')), ('operator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='用户')), ], options={ 'verbose_name': '升级历史记录', 'verbose_name_plural': '升级历史记录', 'db_table': 'omp_upgrade_history', }, ), migrations.CreateModel( name='UpgradeDetail', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('upgrade_state', models.IntegerField(choices=[(0, '等待升级'), (1, '正在升级'), (2, '升级成功'), (3, '升级失败')], default=0, verbose_name='升级结果')), ('union_server', models.CharField(max_length=199, verbose_name='唯一服务实例:ip-app_name(适配hadoop,单服务裂开多个服务)')), ('path_info', models.JSONField(default=dict, verbose_name='服务包备份路径信息')), ('handler_info', models.JSONField(default=dict, verbose_name='升级步骤信息')), ('message', models.TextField(default='', verbose_name='升级日志信息')), ('has_rollback', models.BooleanField(default=False, verbose_name='是否已回滚')), ('current_app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='current_app_set', to='db_models.applicationhub', verbose_name='升级前服务')), ('history', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.upgradehistory', verbose_name='升级历史记录')), ('service', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.service', verbose_name='服务')), ('target_app', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='target_app_set', to='db_models.applicationhub', verbose_name='升级目标服务')), ], options={ 'verbose_name': '单个服务升级记录', 'verbose_name_plural': '单个服务升级记录', 'db_table': 'omp_upgrade_detail', }, ), migrations.CreateModel( name='RollbackHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('rollback_state', models.IntegerField(choices=[(0, '等待回滚'), (1, '正在回滚'), (2, '回滚成功'), (3, '回滚失败')], default=0, verbose_name='回滚结果')), ('env', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='所属环境')), ('operator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='用户')), ], options={ 'verbose_name': '回滚历史记录', 'verbose_name_plural': '回滚历史记录', 'db_table': 'omp_rollback_history', }, ), migrations.CreateModel( name='RollbackDetail', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('rollback_state', models.IntegerField(choices=[(0, '等待回滚'), (1, '正在回滚'), (2, '回滚成功'), (3, '回滚失败')], default=0, verbose_name='回滚结果')), ('path_info', models.JSONField(default=dict, verbose_name='服务包备份路径信息')), ('handler_info', models.JSONField(default=dict, verbose_name='回滚步骤信息')), ('message', models.TextField(default='', verbose_name='回滚日志信息')), ('history', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.rollbackhistory', verbose_name='回滚历史记录')), ('upgrade', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.upgradedetail', verbose_name='升级记录')), ], options={ 'verbose_name': '单个服务回滚记录', 'verbose_name_plural': '单个服务回滚记录', 'db_table': 'omp_rollback_detail', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0010_backuphistory_backupsetting.py ================================================ # Generated by Django 3.1.4 on 2022-01-11 11:01 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0009_auto_20211228_1603'), ] operations = [ migrations.CreateModel( name='BackupHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('backup_name', models.CharField(max_length=128, verbose_name='备份任务名称')), ('content', models.JSONField(verbose_name='备份内容(服务名):["mysql","arangodb"]')), ('result', models.IntegerField(choices=[('成功', 1), ('备份中', 2), ('失败', 0)], default=2, verbose_name='结果')), ('message', models.JSONField(default=dict, verbose_name='返回信息')), ('file_name', models.CharField(default='', max_length=128, verbose_name='备份文件名')), ('file_size', models.CharField(default='0', max_length=64, verbose_name='备份文件大小, MB')), ('expire_time', models.DateTimeField(null=True, verbose_name='过期时间')), ('file_deleted', models.BooleanField(default=False, verbose_name='文件是否被删除')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='记录生成时间')), ('env_id', models.IntegerField(default=0, verbose_name='环境id')), ('retain_path', models.TextField(default='/data/backups/', verbose_name='文件保存路径')), ('operation', models.CharField(default='定时任务执行', max_length=32, verbose_name='操作方式')), ('send_email_result', models.IntegerField(choices=[('发送成功', 1), ('发送中', 2), ('发送失败', 0), ('未发送', 3)], default=3, verbose_name='邮件推送状态')), ('email_fail_reason', models.TextField(default='', verbose_name='邮件推送失败原因')), ], options={ 'verbose_name': '备份历史记录', 'verbose_name_plural': '备份历史记录', 'db_table': 'omp_backup_history', }, ), migrations.CreateModel( name='BackupSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('backup_service', models.JSONField(verbose_name='备份服务名称')), ('is_on', models.BooleanField(default=False, verbose_name='是否开启')), ('crontab_detail', models.JSONField(verbose_name='定时任务详情')), ('retain_day', models.IntegerField(default=1, verbose_name='文件保存天数')), ('retain_path', models.TextField(verbose_name='文件保存路径')), ('env_id', models.IntegerField(default=0, verbose_name='环境id')), ], options={ 'verbose_name': '备份设置', 'verbose_name_plural': '备份设置', 'db_table': 'omp_backup_setting', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0011_auto_20220112_1607.py ================================================ # Generated by Django 3.1.4 on 2022-01-12 16:07 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0010_backuphistory_backupsetting'), ] operations = [ migrations.RemoveField( model_name='backupsetting', name='backup_service', ), migrations.AddField( model_name='backupsetting', name='backup_instances', field=models.JSONField(default=dict, verbose_name='备份服务实例名称'), ), ] ================================================ FILE: omp_server/db_models/migrations/0012_auto_20220112_1653.py ================================================ # Generated by Django 3.1.4 on 2022-01-12 16:53 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0011_auto_20220112_1607'), ] operations = [ migrations.AddField( model_name='backuphistory', name='created', field=models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间'), ), migrations.AddField( model_name='backuphistory', name='modified', field=models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间'), ), ] ================================================ FILE: omp_server/db_models/migrations/0013_merge_20220114_1838.py ================================================ # Generated by Django 3.1.4 on 2022-01-14 18:38 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('db_models', '0010_auto_20220114_1830'), ('db_models', '0012_auto_20220112_1653'), ] operations = [ ] ================================================ FILE: omp_server/db_models/migrations/0014_auto_20220121_1616.py ================================================ # Generated by Django 3.1.4 on 2022-01-21 16:16 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0013_merge_20220114_1838'), ] operations = [ migrations.CreateModel( name='UserLoginLog', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=128, verbose_name='Username')), ('login_time', models.DateTimeField(blank=True, null=True, verbose_name='Login time')), ('ip', models.CharField(blank=True, max_length=32, null=True, verbose_name='Login ip')), ('role', models.CharField(blank=True, max_length=128, null=True, verbose_name='role ')), ], options={ 'verbose_name': '用户登陆记录', 'verbose_name_plural': '用户登陆记录', 'db_table': 'omp_login_log', }, ), migrations.AlterField( model_name='backuphistory', name='content', field=models.JSONField(verbose_name='备份内容(实例名):["mysql1","arangodb2"]'), ), migrations.AlterField( model_name='backuphistory', name='retain_path', field=models.TextField(default='/data/omp/data/backup/', verbose_name='文件保存路径'), ), migrations.AlterField( model_name='backupsetting', name='retain_path', field=models.TextField(default='/data/omp/data/backup/', verbose_name='文件保存路径'), ), ] ================================================ FILE: omp_server/db_models/migrations/0015_executionrecord.py ================================================ # Generated by Django 3.1.4 on 2022-01-24 17:50 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0014_auto_20220121_1616'), ] operations = [ migrations.CreateModel( name='ExecutionRecord', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('module', models.CharField(choices=[('MainInstallHistory', '安装'), ('UpgradeHistory', '升级'), ('RollbackHistory', '回滚')], default='MainInstallHistory', max_length=32, verbose_name='执行记录的module名')), ('module_id', models.IntegerField(default=0, verbose_name='执行记录的id')), ('operator', models.CharField(default='admin', max_length=150, verbose_name='操作用户')), ('state', models.CharField(choices=[('INSTALL_STATUS_READY', '等待安装'), ('INSTALL_STATUS_INSTALLING', '正在安装'), ('INSTALL_STATUS_SUCCESS', '安装成功'), ('INSTALL_STATUS_FAILED', '安装失败'), ('INSTALL_STATUS_REGISTER', '正在注册'), ('UPGRADE_WAIT', '等待升级'), ('UPGRADE_ING', '正在升级'), ('UPGRADE_SUCCESS', '升级成功'), ('UPGRADE_FAIL', '升级失败'), ('ROLLBACK_WAIT', '等待回滚'), ('ROLLBACK_ING', '正在回滚'), ('ROLLBACK_SUCCESS', '回滚成功'), ('ROLLBACK_FAIL', '回滚失败')], default='INSTALL_STATUS_READY', max_length=32, verbose_name='状态')), ('count', models.IntegerField(default=0, verbose_name='服务数量')), ('end_time', models.DateTimeField(null=True, verbose_name='更新时间')), ], options={ 'verbose_name': '执行记录', 'verbose_name_plural': '执行记录', 'db_table': 'omp_execution_record', }, ) ] ================================================ FILE: omp_server/db_models/migrations/0016_auto_20220125_1800.py ================================================ # Generated by Django 3.1.4 on 2022-01-25 18:00 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0015_executionrecord'), ] operations = [ migrations.AlterField( model_name='executionrecord', name='module_id', field=models.CharField(default='0', max_length=36, verbose_name='执行记录的id'), ) ] ================================================ FILE: omp_server/db_models/migrations/0017_selfhealinghistory_selfhealingsetting.py ================================================ # Generated by Django 3.1.4 on 2022-01-28 14:50 from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('db_models', '0016_auto_20220125_1800'), ] operations = [ migrations.CreateModel( name='SelfHealingSetting', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('used', models.BooleanField(default=False, verbose_name='是否启用')), ('alert_count', models.IntegerField(default=1, verbose_name='触发自愈的告警次数')), ('max_healing_count', models.IntegerField(default=5, verbose_name='最多自愈操作次数')), ('env', models.ForeignKey(help_text='环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='环境')), ], options={ 'verbose_name': '自愈设置', 'verbose_name_plural': '自愈设置', 'db_table': 'omp_self_healing_setting', }, ), migrations.CreateModel( name='SelfHealingHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('is_read', models.IntegerField(default=0, verbose_name='此消息是否已读,0-未读;1-已读')), ('host_ip', models.CharField(max_length=64, verbose_name='自愈主机ip')), ('service_name', models.CharField(default='', max_length=64, verbose_name='自愈服务名称')), ('instance_name', models.CharField(default='', help_text='自愈实例名称', max_length=128, verbose_name='自愈实例名称')), ('state', models.IntegerField(choices=[(2, '自愈中'), (0, '自愈失败'), (1, '自愈成功')], default=2, verbose_name='自愈状态')), ('healing_count', models.IntegerField(default=0, verbose_name='已运行自愈次数')), ('start_time', models.DateTimeField(null=True, verbose_name='自愈开始时间')), ('end_time', models.DateTimeField(null=True, verbose_name='自愈结束时间')), ('healing_log', models.JSONField(default=dict, verbose_name='自愈日志')), ('fingerprint', models.CharField(max_length=64, verbose_name='关联告警唯一值')), ('alert_time', models.DateTimeField(null=True, verbose_name='关联告警时间,与fingerprint确定同一次告警')), ('alert_content', models.TextField(default='', verbose_name='告警日志内容')), ('monitor_log', models.TextField(default='', verbose_name='grafana日志url')), ('service_en_type', models.CharField(default='', max_length=64, verbose_name='服务类型,self_dev&component&database')), ('env', models.ForeignKey(help_text='环境', null=True, on_delete=django.db.models.deletion.SET_NULL, to='db_models.env', verbose_name='环境')), ], options={ 'verbose_name': '自愈历史记录', 'verbose_name_plural': '自愈历史记录', 'db_table': 'omp_self_healing_history', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0018_userloginlog_request_result.py ================================================ # Generated by Django 3.1.4 on 2022-02-08 15:37 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0017_selfhealinghistory_selfhealingsetting'), ] operations = [ migrations.AddField( model_name='userloginlog', name='request_result', field=models.CharField(blank=True, help_text='请求结果', max_length=512, null=True, verbose_name='请求结果'), ), ] ================================================ FILE: omp_server/db_models/migrations/0019_toolexecutedetailhistory_toolexecutemainhistory_toolinfo_uploadfilehistory.py ================================================ # Generated by Django 3.1.4 on 2022-02-16 11:25 from django.conf import settings from django.db import migrations, models import django.db.models.deletion class Migration(migrations.Migration): dependencies = [ ('db_models', '0018_userloginlog_request_result'), ] operations = [ migrations.CreateModel( name='ToolInfo', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('name', models.CharField(help_text='实用工具名称', max_length=128)), ('kind', models.IntegerField(choices=[(0, '管理工具'), (1, '检查工具'), (2, '安全工具'), (3, '其他工具')], default=0, help_text='实用工具分类', verbose_name='实用工具分类')), ('script_type', models.IntegerField(choices=[(0, 'python3'), (1, 'shell')], default=0, help_text='脚本类型', verbose_name='脚本类型')), ('target_name', models.CharField(default='host', help_text='脚本执行的目标对象', max_length=128, verbose_name='脚本执行的目标对象')), ('source_package_md5', models.CharField(blank=True, help_text='源码包md5值', max_length=32, null=True, verbose_name='源码包md5值')), ('source_package_path', models.CharField(max_length=128, verbose_name='源码包相对路径')), ('tool_folder_path', models.CharField(help_text='实用工具目录相对路径', max_length=128, verbose_name='实用工具目录相对路径')), ('script_path', models.CharField(help_text='脚本相对路径', max_length=128, verbose_name='脚本相对路径')), ('send_package', models.JSONField(default=list, help_text='需要发送的文件相对路径', max_length=128, verbose_name='需要发送的文件相对路径')), ('readme_info', models.TextField(blank=True, help_text='readme信息', null=True, verbose_name='readme信息')), ('template_filepath', models.JSONField(default=list, help_text='模板文件相对路径', verbose_name='模板文件相对路径')), ('obj_connection_args', models.JSONField(default=list, verbose_name='目标对象连接信息')), ('script_args', models.JSONField(default=list, verbose_name='脚本执行参数')), ('output', models.IntegerField(choices=[(0, '终端输出'), (1, '文件输出')], default=0, help_text='脚本的输出类型', verbose_name='脚本的输出类型')), ('description', models.TextField(help_text='描述信息', verbose_name='描述信息')), ], options={ 'verbose_name': '实用工具基本信息表', 'verbose_name_plural': '实用工具基本信息表', 'db_table': 'omp_tool_info', }, ), migrations.CreateModel( name='UploadFileHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('module', models.CharField(default='', max_length=32, verbose_name='需要上传文件的model')), ('module_id', models.IntegerField(default=0, verbose_name='需要上传文件的model id')), ('union_id', models.CharField(default='', max_length=64, verbose_name='文件md5值')), ('storage_klass', models.CharField(default='location', max_length=64, verbose_name='存储方式')), ('relative_path', models.TextField(default='', verbose_name='文件存储相对路径')), ('file_name', models.CharField(default='', max_length=64, verbose_name='文件名称')), ('file_size', models.CharField(default='0K', max_length=16, verbose_name='文件大小')), ('file_url', models.TextField(default='', verbose_name='文件访问路径')), ('deleted', models.BooleanField(default=False, verbose_name='删除')), ('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='用户')), ], options={ 'verbose_name': '上传文件记录', 'verbose_name_plural': '上传文件记录', 'db_table': 'omp_upload_file', }, ), migrations.CreateModel( name='ToolExecuteMainHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('task_name', models.CharField(help_text='任务标题', max_length=128, null=True, verbose_name='任务标题')), ('operator', models.CharField(blank=True, help_text='操作人', max_length=128, null=True, verbose_name='操作人')), ('status', models.IntegerField(choices=[(0, '待执行'), (1, '执行中'), (2, '执行成功'), (3, '执行失败')], default=0, help_text='main执行状态', verbose_name='main执行状态')), ('start_time', models.DateTimeField(auto_now_add=True, help_text='开始时间', null=True, verbose_name='开始时间')), ('end_time', models.DateTimeField(help_text='结束时间', null=True, verbose_name='结束时间')), ('form_answer', models.JSONField(default=dict, verbose_name='任务表单提交结果')), ('tool', models.ForeignKey(help_text='实用工具对象', on_delete=django.db.models.deletion.CASCADE, to='db_models.toolinfo')), ], options={ 'verbose_name': '实用工具执行记录', 'verbose_name_plural': '实用工具执行记录', 'db_table': 'omp_tool_execute_main_history', }, ), migrations.CreateModel( name='ToolExecuteDetailHistory', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('target_ip', models.CharField(help_text='目标IP地址', max_length=64, verbose_name='目标IP地址')), ('time_out', models.IntegerField(default=60, verbose_name='超时时间')), ('run_user', models.CharField(default='', max_length=64, verbose_name='执行用户')), ('status', models.IntegerField(choices=[(0, '待执行'), (1, '执行中'), (2, '执行成功'), (3, '执行失败'), (4, '执行超时')], default=0, help_text='detail执行状态', verbose_name='detail执行状态')), ('execute_args', models.JSONField(default=dict, help_text='执行参数信息', verbose_name='执行参数信息')), ('execute_log', models.TextField(help_text='执行日志', verbose_name='执行日志')), ('output', models.JSONField(default=dict, verbose_name='脚本输出内容')), ('main_history', models.ForeignKey(help_text='实用工具对象', on_delete=django.db.models.deletion.CASCADE, to='db_models.toolexecutemainhistory')), ], options={ 'verbose_name': '实用工具执行详情表', 'verbose_name_plural': '实用工具执行详情表', 'db_table': 'omp_tool_execute_detail_history', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0020_init_tools.py ================================================ # Generated by Django 3.1.4 on 2022-02-22 16:59 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('db_models', '0019_toolexecutedetailhistory_toolexecutemainhistory_toolinfo_uploadfilehistory'), ] operations = [ ] ================================================ FILE: omp_server/db_models/migrations/0021_customscript.py ================================================ # Generated by Django 3.1.4 on 2022-02-23 15:36 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0020_init_tools'), ] operations = [ migrations.CreateModel( name='CustomScript', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('created', models.DateTimeField(auto_now_add=True, help_text='创建时间', null=True, verbose_name='创建时间')), ('modified', models.DateTimeField(auto_now=True, help_text='更新时间', null=True, verbose_name='更新时间')), ('script_name', models.CharField(help_text='自定义脚本名称', max_length=32, verbose_name='脚本名称')), ('script_content', models.TextField(help_text='脚本内容', max_length=10000, verbose_name='脚本内容')), ('metrics', models.JSONField(help_text='指标列表', verbose_name='指标列表')), ('metric_num', models.IntegerField(help_text='metric数量', null=True, verbose_name='metric数量')), ('scrape_interval', models.IntegerField(default=60, help_text='prometheus探测周期', verbose_name='探测周期')), ('enabled', models.BooleanField(default=1, help_text='1位启用,0为禁用', verbose_name='是否启用')), ('description', models.TextField(help_text='脚本描述', max_length=1024, verbose_name='脚本描述')), ('bound_hosts', models.JSONField(help_text='已下发主机列表', verbose_name='已下发主机列表')), ], options={ 'verbose_name': '自定义脚本', 'verbose_name_plural': '自定义脚本', 'db_table': 'omp_custom_script', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0022_alertrule_rule.py ================================================ # Generated by Django 3.1.4 on 2022-02-25 09:44 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0021_customscript'), ] operations = [ migrations.CreateModel( name='AlertRule', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('env_id', models.IntegerField(default=1, verbose_name='环境id')), ('expr', models.TextField(verbose_name='监控指标表达式,报警语法')), ('threshold_value', models.FloatField(verbose_name='阈值的数值')), ('compare_str', models.CharField(max_length=64, verbose_name='比较符')), ('for_time', models.CharField(max_length=64, verbose_name='持续一段时间获取不到信息就触发告警')), ('severity', models.CharField(max_length=64, verbose_name='告警级别')), ('alert', models.TextField(verbose_name='标题,自定义摘要')), ('service', models.CharField(max_length=255, verbose_name='指标所属服务名称')), ('status', models.IntegerField(default=0, verbose_name='启用状态')), ('name', models.CharField(max_length=255, null=True, verbose_name='内置指标名称')), ('quota_type', models.IntegerField(choices=[(0, 'builtins'), (1, 'custom'), (2, 'log')], default=0, verbose_name='指标的类型')), ('labels', models.JSONField(null=True, verbose_name='额外指定标签')), ('summary', models.TextField(null=True, verbose_name='描述, 告警指标描述')), ('description', models.TextField(null=True, verbose_name='描述, 告警指标描述')), ('create_time', models.DateTimeField(auto_now_add=True, verbose_name='告警规则入库时间')), ('update_time', models.DateTimeField(auto_now_add=True, verbose_name='告警规则更新时间')), ], options={ 'verbose_name': '自定义告警规则', 'verbose_name_plural': '自定义告警规则', 'db_table': 'omp_alert_ruler', }, ), migrations.CreateModel( name='Rule', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(max_length=255, verbose_name='指标名称')), ('expr', models.TextField(verbose_name='监控指标表达式,报警语法')), ('service', models.CharField(max_length=255, verbose_name='服务名称')), ('description', models.TextField(null=True, verbose_name='描述')), ], options={ 'verbose_name': '规则表达式', 'verbose_name_plural': '规则表达式', 'db_table': 'omp_rule', }, ), ] ================================================ FILE: omp_server/db_models/migrations/0023_auto_20220225_1747.py ================================================ # Generated by Django 3.1.4 on 2022-02-25 17:47 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0022_alertrule_rule'), ] operations = [ migrations.AddField( model_name='host', name='ntpd_server', field=models.GenericIPAddressField(default='127.0.0.1', verbose_name='时间同步服务器地址'), ), migrations.AddField( model_name='host', name='ntpdate_install_status', field=models.CharField(choices=[(0, '执行成功'), (1, '未执行'), (2, '执行中'), (3, '执行失败')], default=1, max_length=16, verbose_name='安装ntpdate状态'), ), migrations.AddField( model_name='host', name='use_ntpd', field=models.BooleanField(default=False, verbose_name='是否开启时间同步服务'), ), migrations.AlterField( model_name='host', name='host_agent', field=models.CharField(choices=[(0, '正常'), (1, '重启中'), (2, '启动失败'), (3, '部署中'), (4, '部署失败'), (5, '删除中')], default=3, help_text='主机Agent状态', max_length=16, verbose_name='主机Agent状态'), ), migrations.AlterField( model_name='host', name='monitor_agent', field=models.CharField(choices=[(0, '正常'), (1, '重启中'), (2, '启动失败'), (3, '部署中'), (4, '部署失败'), (5, '删除中')], default=3, help_text='监控Agent状态', max_length=16, verbose_name='监控Agent状态'), ), ] ================================================ FILE: omp_server/db_models/migrations/0024_auto_20220226_1300.py ================================================ # Generated by Django 3.1.4 on 2022-02-27 13:00 import os from django.conf import settings from django.db import migrations, models from db_models.models import ToolInfo from tool.find_tools import find_tools_package def update_tool_logo(apps, schema_editor): tools = ToolInfo.objects.all() for tool in tools: folder_path = os.path.join( settings.PROJECT_DIR, "package_hub", tool.tool_folder_path ) if os.path.exists(os.path.join(folder_path, 'logo.svg')): tool.logo = os.path.join(tool.tool_folder_path, 'logo.svg') tool.save() find_tools_package() class Migration(migrations.Migration): dependencies = [ ('db_models', '0023_auto_20220225_1747'), ] operations = [ migrations.AddField( model_name='toolinfo', name='logo', field=models.URLField(default='', verbose_name='logo url'), ), migrations.RunPython(update_tool_logo), ] ================================================ FILE: omp_server/db_models/migrations/0025_alertrule_forbidden.py ================================================ # Generated by Django 3.1.4 on 2022-02-28 19:31 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0024_auto_20220226_1300'), ] operations = [ migrations.AddField( model_name='alertrule', name='forbidden', field=models.IntegerField(default=1, verbose_name='禁止删除'), ), ] ================================================ FILE: omp_server/db_models/migrations/0026_alertrule_hash_data.py ================================================ # Generated by Django 3.1.4 on 2022-03-03 13:28 from django.db import migrations, models from uuid import uuid4 from db_models.models import AlertRule import hashlib def get_hash_value(expr, severity): data = expr + severity hash_data = hashlib.md5(data.encode(encoding='UTF-8')).hexdigest() return hash_data def update_hash_data(apps, schema_editor): alert_rulers = AlertRule.objects.all() for alert in alert_rulers: hash_data = get_hash_value(alert.expr, alert.severity) alert.hash_data = hash_data alert.save() class Migration(migrations.Migration): dependencies = [ ('db_models', '0025_alertrule_forbidden'), ] operations = [ migrations.AddField( model_name='alertrule', name='hash_data', field=models.CharField(null=True, blank=True, unique=True, verbose_name='唯一hash值', max_length=255), ), migrations.RunPython(update_hash_data), ] ================================================ FILE: omp_server/db_models/migrations/0026_auto_20220303_1527.py ================================================ # Generated by Django 3.1.4 on 2022-03-03 15:27 from django.db import migrations, models import django.db.models.manager from db_models.models import Service def combine_names(apps, schema_editor): for service in Service.objects.all(): if service.service.app_name == "hadoop" and service.service_split == 0: if service.service_instance_name.startswith("hadoop"): service.service_split = 1 else: service.service_split = 2 service.save() class Migration(migrations.Migration): dependencies = [ ('db_models', '0025_alertrule_forbidden'), ] operations = [ migrations.AlterModelManagers( name='service', managers=[ ('split_objects', django.db.models.manager.Manager()), ], ), migrations.AddField( model_name='service', name='service_split', field=models.IntegerField(choices=[(1, '拆分前'), (2, '拆分后'), (0, '未拆分')], default=0, help_text='拆分服务前对象', verbose_name='拆分服务前对象'), ), migrations.RunPython(combine_names) ] ================================================ FILE: omp_server/db_models/migrations/0027_merge_20220304_2000.py ================================================ # Generated by Django 3.1.4 on 2022-03-04 20:00 from django.db import migrations class Migration(migrations.Migration): dependencies = [ ('db_models', '0026_auto_20220303_1527'), ('db_models', '0026_alertrule_hash_data'), ] operations = [ ] ================================================ FILE: omp_server/db_models/migrations/0028_auto_20220304_2001.py ================================================ # Generated by Django 3.1.4 on 2022-03-04 20:01 from django.db import migrations, models from db_models.models import MainInstallHistory, UpgradeHistory, \ RollbackHistory from db_models.receivers.execution_record import create_execution_record def update_execution_record(apps, schema_editor): for model in [MainInstallHistory, UpgradeHistory, RollbackHistory]: histories = model.objects.all() for history in histories: create_execution_record(history) class Migration(migrations.Migration): dependencies = [ ('db_models', '0027_merge_20220304_2000'), ] operations = [ migrations.AddField( model_name='upgradehistory', name='pre_upgrade_result', field=models.JSONField(default=dict, verbose_name='升级前置信息'), ), migrations.AddField( model_name='upgradehistory', name='pre_upgrade_state', field=models.IntegerField(choices=[(0, '等待升级'), (1, '正在升级'), (2, '升级成功'), (3, '升级失败')], default=0, verbose_name='升级前置结果'), ), migrations.RunPython(update_execution_record), ] ================================================ FILE: omp_server/db_models/migrations/0029_auto_20230110_1739.py ================================================ # Generated by Django 3.1.4 on 2023-01-10 17:39 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0028_auto_20220304_2001'), ] operations = [ migrations.AddField( model_name='userprofile', name='role', field=models.CharField(blank=True, help_text='用户角色', max_length=128, null=True, verbose_name='用户角色'), ), migrations.AlterField( model_name='alertrule', name='hash_data', field=models.CharField(blank=True, max_length=255, null=True, verbose_name='唯一hash值'), ), ] ================================================ FILE: omp_server/db_models/migrations/0030_auto_20230711_1739.py ================================================ # Generated by Django 3.1.4 on 2023-05-11 09:33 import django.core.validators from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0029_auto_20230110_1739'), ] operations = [ migrations.CreateModel( name='WaitSelfHealing', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('service_name', models.CharField(default='', max_length=64, verbose_name='自愈服务名称')), ('repair_ser', models.JSONField(default=dict, verbose_name='自愈缓存服务详情')), ('repair_status', models.IntegerField(default=0, verbose_name='自愈缓存服务')), ], options={ 'verbose_name': '自愈等待队列', 'verbose_name_plural': '自愈等待队列', 'db_table': 'omp_wait_self_healing', }, ), migrations.RemoveField( model_name='selfhealinghistory', name='env', ), migrations.RemoveField( model_name='selfhealinghistory', name='fingerprint', ), migrations.RemoveField( model_name='selfhealinghistory', name='service_en_type', ), migrations.RemoveField( model_name='selfhealingsetting', name='alert_count', ), migrations.RemoveField( model_name='selfhealingsetting', name='env', ), migrations.AddField( model_name='selfhealingsetting', name='fresh_rate', field=models.IntegerField(default=10, validators=[django.core.validators.MaxValueValidator(60)], verbose_name='周期内采集告警消息频次'), ), migrations.AddField( model_name='selfhealingsetting', name='instance_tp', field=models.IntegerField(choices=[(0, '启动'), (1, '重新启动')], default=0, verbose_name='实例类别'), ), migrations.AddField( model_name='selfhealingsetting', name='repair_instance', field=models.JSONField(default=dict, verbose_name='修复实例'), ), migrations.AlterField( model_name='backuphistory', name='retain_path', field=models.TextField(blank=True, default='/data/omp/data/backup/', max_length=256, null=True, verbose_name='文件保存路径'), ), migrations.AlterField( model_name='selfhealinghistory', name='healing_log', field=models.TextField(default='', verbose_name='自愈日志'), ), migrations.AlterField( model_name='selfhealingsetting', name='max_healing_count', field=models.IntegerField(default=5, validators=[django.core.validators.MaxValueValidator(20)], verbose_name='最多自愈操作次数'), ), ] ================================================ FILE: omp_server/db_models/migrations/0031_auto_20230921_1128.py ================================================ # Generated by Django 3.1.4 on 2023-09-21 11:28 from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('db_models', '0030_auto_20230711_1739'), ] operations = [ migrations.CreateModel( name='BackupCustom', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('field_k', models.CharField(max_length=64, verbose_name='自定义字段k')), ('field_v', models.CharField(max_length=256, verbose_name='自定义字段v')), ('notes', models.CharField(max_length=32, verbose_name='备注')), ], options={ 'verbose_name': '自定义备份', 'verbose_name_plural': '自定义备份', 'db_table': 'omp_backup_custom', }, ), migrations.RemoveField( model_name='backuphistory', name='email_fail_reason', ), migrations.RemoveField( model_name='backuphistory', name='env_id', ), migrations.RemoveField( model_name='backuphistory', name='operation', ), migrations.RemoveField( model_name='backuphistory', name='send_email_result', ), migrations.RemoveField( model_name='backupsetting', name='env_id', ), migrations.AddField( model_name='backuphistory', name='extend_field', field=models.JSONField(default=dict, verbose_name='冗余字段'), ), migrations.AddField( model_name='backuphistory', name='remote_path', field=models.CharField(blank=True, max_length=256, null=True, verbose_name='远端备份路径'), ), migrations.AlterField( model_name='backuphistory', name='content', field=models.CharField(default='', max_length=256, verbose_name='备份实例'), ), migrations.AlterField( model_name='backuphistory', name='message', field=models.TextField(default='', max_length=512, verbose_name='返回信息'), ), migrations.AlterField( model_name='backuphistory', name='result', field=models.IntegerField(choices=[(1, '成功'), (2, '备份中'), (0, '失败')], default=2, verbose_name='结果'), ), migrations.AlterField( model_name='backuphistory', name='retain_path', field=models.TextField(default='/data/omp/data/backup/', max_length=256, verbose_name='文件保存路径'), ), migrations.AlterField( model_name='backupsetting', name='retain_path', field=models.CharField(default='/data/omp/data/backup/', max_length=256, verbose_name='文件保存路径'), ), migrations.AddField( model_name='backupsetting', name='backup_custom', field=models.ManyToManyField(to='db_models.BackupCustom'), ), ] ================================================ FILE: omp_server/db_models/migrations/__init__.py ================================================ ================================================ FILE: omp_server/db_models/mixins.py ================================================ """ 模型混入类 """ from django.db import models class TimeStampMixin(models.Model): """ 创建、更新时间混入类 """ created = models.DateTimeField( "创建时间", null=True, auto_now_add=True, help_text="创建时间") modified = models.DateTimeField( "更新时间", null=True, auto_now=True, help_text="更新时间") class Meta: abstract = True class DeleteMixin(models.Model): """ 软删除混入类 """ is_deleted = models.BooleanField(default=False, help_text="软删除") def delete(self, using=None, soft=True, *args, **kwargs): if soft: self.is_deleted = True self.save(using=using) else: return super(DeleteMixin, self).delete(using=using, *args, **kwargs) class Meta: abstract = True class UpgradeStateChoices(models.IntegerChoices): UPGRADE_WAIT = 0, "等待升级" UPGRADE_ING = 1, "正在升级" UPGRADE_SUCCESS = 2, "升级成功" UPGRADE_FAIL = 3, "升级失败" class UpgradeStateMixin(models.Model): upgrade_state = models.IntegerField( "升级结果", choices=UpgradeStateChoices.choices, default=UpgradeStateChoices.UPGRADE_WAIT ) class Meta: abstract = True class RollbackStateChoices(models.IntegerChoices): ROLLBACK_WAIT = 0, "等待回滚" ROLLBACK_ING = 1, "正在回滚" ROLLBACK_SUCCESS = 2, "回滚成功" ROLLBACK_FAIL = 3, "回滚失败" class RollBackStateMixin(models.Model): rollback_state = models.IntegerField( "回滚结果", choices=RollbackStateChoices.choices, default=RollbackStateChoices.ROLLBACK_WAIT ) class Meta: abstract = True ================================================ FILE: omp_server/db_models/models/__init__.py ================================================ from .backup import BackupSetting, BackupHistory, BackupCustom from .email import EmailSMTPSetting, ModuleSendEmailSetting from .env import Env from .execution import ExecutionRecord from .host import Host, HostOperateLog from .inspection import InspectionHistory, InspectionCrontab, InspectionReport from .install import MainInstallHistory, PreInstallHistory, \ DetailInstallHistory, PostInstallHistory, DeploymentPlan from .monitor import MonitorUrl, Alert, Maintain, GrafanaMainPage, \ AlertSendWaySetting from .product import Labels, UploadPackageHistory, ProductHub, \ ApplicationHub, Product from .service import ServiceConnectInfo, ClusterInfo, Service, ServiceHistory from .threshold import HostThreshold, ServiceThreshold, ServiceCustomThreshold,AlertRule,Rule from .tool import ToolInfo, ToolExecuteMainHistory, ToolExecuteDetailHistory from .upload import UploadFileHistory from .user import UserProfile, OperateLog, UserLoginLog from .upgrade import UpgradeHistory, UpgradeDetail, RollbackHistory, \ RollbackDetail from .self_heal import SelfHealingSetting, SelfHealingHistory, WaitSelfHealing from .custom_metric import CustomScript __all__ = [ # 邮箱设置 EmailSMTPSetting, ModuleSendEmailSetting, # 环境 Env, # 主机 Host, HostOperateLog, # 巡检 InspectionHistory, InspectionCrontab, InspectionReport, # 安装 MainInstallHistory, PreInstallHistory, PostInstallHistory, DetailInstallHistory, DeploymentPlan, # 监控 MonitorUrl, Alert, Maintain, GrafanaMainPage, AlertSendWaySetting, # 产品 Labels, UploadPackageHistory, ProductHub, ApplicationHub, Product, # 服务 ServiceConnectInfo, ClusterInfo, Service, ServiceHistory, # 阈值 HostThreshold, ServiceThreshold, ServiceCustomThreshold, # 用户 UserProfile, OperateLog, UserLoginLog, # 升级 UpgradeHistory, UpgradeDetail, # 回滚 RollbackHistory, RollbackDetail, # 备份 BackupSetting, BackupHistory, # 自愈 SelfHealingHistory, SelfHealingSetting, WaitSelfHealing, # 执行记录 ExecutionRecord, # 小工具 ToolInfo, ToolExecuteMainHistory, ToolExecuteDetailHistory, # 上传文件公共表 UploadFileHistory, Alert, AlertRule, # 自定义脚本 CustomScript, Alert, AlertRule ] ================================================ FILE: omp_server/db_models/models/backup.py ================================================ import os from django.db import models from db_models.mixins import TimeStampMixin class BackupCustom(models.Model): field_k = models.CharField("自定义字段k", max_length=64, null=False) field_v = models.CharField("自定义字段v", max_length=256, null=False) notes = models.CharField("备注", max_length=32, default="") class Meta: db_table = 'omp_backup_custom' verbose_name = verbose_name_plural = '自定义备份' class BackupSetting(models.Model): # 校验是否安装该服务,支持的服务 backup_instances = models.JSONField("备份服务实例名称", default=dict) is_on = models.BooleanField("是否开启", default=False) crontab_detail = models.JSONField("定时任务详情") retain_day = models.IntegerField("文件保存天数", default=1) retain_path = models.CharField("文件保存路径", default="/data/omp/data/backup/", null=False, max_length=256) backup_custom = models.ManyToManyField(BackupCustom) class Meta: db_table = 'omp_backup_setting' verbose_name = verbose_name_plural = '备份设置' class BackupHistory(TimeStampMixin): backup_name = models.CharField("备份任务名称", max_length=128) content = models.CharField('备份实例', max_length=256, default="") SUCCESS = 1 ING = 2 FAIL = 0 RESULT_CHOICES = ( (SUCCESS, "成功"), (ING, "备份中"), (FAIL, "失败") ) result = models.IntegerField("结果", choices=RESULT_CHOICES, default=ING) message = models.TextField("返回信息", default="", max_length=512) file_name = models.CharField("备份文件名", max_length=128, default="") file_size = models.CharField("备份文件大小, MB", default="0", max_length=64) expire_time = models.DateTimeField("过期时间", null=True) file_deleted = models.BooleanField("文件是否被删除", default=False) create_time = models.DateTimeField("记录生成时间", auto_now_add=True) retain_path = models.TextField( "文件保存路径", default="/data/omp/data/backup/", max_length=256, null=True, blank=True ) remote_path = models.CharField("远端备份路径", max_length=256, null=True, blank=True) extend_field = models.JSONField("冗余字段", default=dict) class Meta: db_table = 'omp_backup_history' verbose_name = verbose_name_plural = '备份历史记录' def fetch_file_kwargs(self): file_path = os.path.join(self.retain_path, self.file_name) return {"path": file_path} ================================================ FILE: omp_server/db_models/models/custom_metric.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author:' Lingyang.guo' # CreateDate: 14:08 import os from django.db import models from db_models.mixins import TimeStampMixin class CustomScript(TimeStampMixin): """ 自定义脚本 """ objects = None script_name = models.CharField( "脚本名称", max_length=32, null=False, help_text="自定义脚本名称") script_content = models.TextField( "脚本内容", max_length=10000, null=False, help_text="脚本内容") metrics = models.JSONField("指标列表", null=False, help_text="指标列表") metric_num = models.IntegerField( "metric数量", help_text="metric数量", null=True) scrape_interval = models.IntegerField( "探测周期", default=60, help_text="prometheus探测周期") enabled = models.BooleanField("是否启用", default=1, help_text="1位启用,0为禁用") description = models.TextField( "脚本描述", max_length=1024, null=False, help_text="脚本描述") bound_hosts = models.JSONField("已下发主机列表", help_text="已下发主机列表") def valid_upload_file(self, *args, **kwargs): # NOQA return "package_hub/custom_scripts" def upload_file_url(self, file_name, **kwargs): # NOQA return os.path.join("package_hub/custom_scripts", file_name) class Meta: """元数据""" db_table = "omp_custom_script" verbose_name = verbose_name_plural = "自定义脚本" ================================================ FILE: omp_server/db_models/models/email.py ================================================ import logging import os import requests from django.db import models from ruamel import yaml from .monitor import AlertSendWaySetting logger = logging.getLogger('server') class EmailSMTPSetting(models.Model): email_host = models.EmailField("邮箱SMTP主机地址:smtp.163.com", null=True) email_port = models.IntegerField("邮箱SMTP端口:465", null=True) email_host_user = models.CharField( "邮箱SMTP服务器用户名:a@163.com", max_length=128, null=True) email_host_password = models.CharField( "邮箱SMTP服务器秘钥", max_length=128, null=True) class Meta: db_table = 'omp_email_smtp_setting' verbose_name = verbose_name_plural = '平台邮件服务器配置' alert_manage_key = { "EMAIL_SEND": "email_host_user", "smtp_auth_username": "email_host_user", "EMAIL_SEND_PASSWORD": "email_host_password", "smtp_auth_password": "email_host_password", "EMAIL_ADDRESS": "email_url", } def get_dict(self): return { "host": self.email_host, "port": self.email_port, "username": self.email_host_user, "password": self.email_host_password } @property def email_url(self): if hasattr(self, "email"): return self.email email_send = AlertSendWaySetting.objects.filter( way_name="email").first() email = "" if email_send and email_send.used: email = email_send.server_url.split(",")[0] setattr(self, "email", email) return email def update_config(self, key): if key in {"EMAIL_SEND_USER", "smtp_from"}: value = self.email_host_user elif key in {"SMTP_SMARTHOST", "smtp_smarthost"}: value = f"{self.email_host}:{self.email_port}" elif key in self.alert_manage_key: value = getattr(self, self.alert_manage_key.get(key)) elif key in {"smtp_hello", "SMTP_HELLO"}: value = ".".join(self.email_host.split(".")[-2:]) elif key == "RECEIVER": value = "cloudwise" else: value = None return value @staticmethod def reload_alert_manage(): from promemonitor.alertmanager import Alertmanager alertmanager_url = Alertmanager.get_alertmanager_config() _basic_auth = Alertmanager().basic_auth try: response = requests.post(f"http://{alertmanager_url}/-/reload", auth=_basic_auth) # NOQA except Exception as e: logger.error(f"重载alertmanager配置出错!错误信息:{str(e)}") return False if response.status_code == 200: return True return False def set_omp_conf(self): # 更新omp.yaml from utils.parse_config import config_file_path with open(config_file_path, "r", encoding="utf8") as fp: content = fp.read() my_yaml = yaml.YAML() code = my_yaml.load(content) for key, value in code.get("alert_manager", {}).items(): if key == "send_email": value = bool(self.email_url) else: value = self.update_config(key) if value is None: continue code["alert_manager"][key] = value with open(config_file_path, "w", encoding="utf8") as fp: my_yaml.dump(code, fp) def set_alert_manage_config(self): # 更新alert manage配置 from omp_server.settings import PROJECT_DIR config_path = os.path.join( PROJECT_DIR, "component/alertmanager/conf/alertmanager.yml") with open(config_path, "r", encoding="utf8") as fp: code = yaml.load(fp.read(), yaml.Loader) for key, value in code.get("global", {}).items(): value = self.update_config(key) if value is None: continue code["global"][key] = value if not self.email_url: for receiver in code.get("receivers"): if "email_configs" in receiver: code.get("receivers")[0].pop("email_configs", {}) else: email_configs = [ { "send_resolved": bool(self.email_url), "to": self.email_url, "headers": {"Subject": "OMP ALERT"}, "html": '{{ template \"email.to.html\" . }}' } ] code.get("receivers")[0]["email_configs"] = email_configs code["templates"] = [ f"{PROJECT_DIR}/component/alertmanager/templates/*tmpl"] with open(config_path, "w", encoding="utf8") as fp: yaml.dump(code, fp, Dumper=yaml.RoundTripDumper) return self.reload_alert_manage() def update_setting_config(self): """ 更新配置文件 :return: """ self.set_omp_conf() return self.set_alert_manage_config(), self.email_url class ModuleSendEmailSetting(models.Model): module = models.CharField( "功能模块:BackupSetting,JobSetting", max_length=64) send_email = models.BooleanField("是否开启邮件推送", default=False) to_users = models.TextField("邮箱接收用户", default="") env_id = models.IntegerField("环境id", default=1) class Meta: db_table = 'omp_module_email_send_setting' verbose_name = verbose_name_plural = '平台邮件发送账号配置' @classmethod def get_email_settings(cls, env_id, module): try: _obj = cls.objects.get(env_id=env_id, module=module) except Exception as e: logger.error(e) logger.error(f"module: {module}, env_id:{env_id}邮箱配置不存在") return None return _obj @classmethod def update_email_settings(cls, env_id, module, send_email, to_users): _obj, _ = cls.objects.get_or_create(env_id=env_id, module=module) _obj.send_email = send_email _obj.to_users = to_users _obj.save() ================================================ FILE: omp_server/db_models/models/env.py ================================================ from django.db import models class Env(models.Model): """ 环境表 """ objects = None name = models.CharField( "环境名称", max_length=256, help_text="环境名称") created = models.DateTimeField( '创建时间', null=True, auto_now_add=True, help_text='创建时间') class Meta: db_table = "omp_env" verbose_name = verbose_name_plural = "环境" ================================================ FILE: omp_server/db_models/models/execution.py ================================================ from django.db import models from db_models.mixins import TimeStampMixin class ModuleChoices(models.TextChoices): INSTALL = "MainInstallHistory", "安装" UPGRADE = "UpgradeHistory", "升级" ROLLBACK = "RollbackHistory", "回滚" class StateChoices(models.TextChoices): MAININSTALLHISTORY_0 = "INSTALL_STATUS_READY", "等待安装" MAININSTALLHISTORY_1 = "INSTALL_STATUS_INSTALLING", "正在安装" MAININSTALLHISTORY_2 = "INSTALL_STATUS_SUCCESS", "安装成功" MAININSTALLHISTORY_3 = "INSTALL_STATUS_FAILED", "安装失败" MAININSTALLHISTORY_4 = "INSTALL_STATUS_REGISTER", "正在注册" UPGRADEHISTORY_0 = "UPGRADE_WAIT", "等待升级" UPGRADEHISTORY_1 = "UPGRADE_ING", "正在升级" UPGRADEHISTORY_2 = "UPGRADE_SUCCESS", "升级成功" UPGRADEHISTORY_3 = "UPGRADE_FAIL", "升级失败" ROLLBACKHISTORY_0 = "ROLLBACK_WAIT", "等待回滚" ROLLBACKHISTORY_1 = "ROLLBACK_ING", "正在回滚" ROLLBACKHISTORY_2 = "ROLLBACK_SUCCESS", "回滚成功" ROLLBACKHISTORY_3 = "ROLLBACK_FAIL", "回滚失败" class ExecutionRecord(TimeStampMixin): # 通过django信号同步生成记录(create_execution_record) module = models.CharField( "执行记录的module名", max_length=32, choices=ModuleChoices.choices, default=ModuleChoices.INSTALL ) # UpgradeHistory.id & MainInstallHistory.operation_uuid module_id = models.CharField("执行记录的id", max_length=36, default="0") operator = models.CharField( "操作用户", max_length=150, default="admin" ) state = models.CharField( "状态", max_length=32, choices=StateChoices.choices, default=StateChoices.MAININSTALLHISTORY_0 ) count = models.IntegerField("服务数量", default=0) end_time = models.DateTimeField("更新时间", null=True) class Meta: db_table = "omp_execution_record" verbose_name = verbose_name_plural = '执行记录' ================================================ FILE: omp_server/db_models/models/host.py ================================================ from django.db import models from db_models.mixins import TimeStampMixin, DeleteMixin from .env import Env class Host(TimeStampMixin, DeleteMixin): """ 主机表 """ AGENT_RUNNING = 0 AGENT_RESTART = 1 AGENT_START_ERROR = 2 AGENT_DEPLOY_ING = 3 AGENT_DEPLOY_ERROR = 4 AGENT_DEPLOY_DELETE = 5 AGENT_STATUS_CHOICES = ( (AGENT_RUNNING, "正常"), (AGENT_RESTART, "重启中"), (AGENT_START_ERROR, "启动失败"), (AGENT_DEPLOY_ING, "部署中"), (AGENT_DEPLOY_ERROR, "部署失败"), (AGENT_DEPLOY_DELETE, "删除中"), ) INIT_SUCCESS = 0 INIT_NOT_EXECUTED = 1 INIT_EXECUTING = 2 INIT_FAILED = 3 INIT_STATUS_CHOICES = ( (INIT_SUCCESS, "成功"), (INIT_NOT_EXECUTED, "未执行"), (INIT_EXECUTING, "执行中"), (INIT_FAILED, "失败") ) NTPDATE_INSTALL_SUCCESS = 0 NTPDATE_NOT_INSTALL = 1 NTPDATE_INSTALLING = 2 NTPDATE_INSTALL_FAILED = 3 NTPDATE_STATUS_CHOICES = ( (NTPDATE_INSTALL_SUCCESS, "执行成功"), (NTPDATE_NOT_INSTALL, "未执行"), (NTPDATE_INSTALLING, "执行中"), (NTPDATE_INSTALL_FAILED, "执行失败") ) objects = None instance_name = models.CharField( "实例名", max_length=64, help_text="实例名", unique=True) ip = models.GenericIPAddressField( "IP地址", help_text="IP地址", unique=True) port = models.IntegerField( "SSH端口", default=22, help_text="SSH端口") username = models.CharField( "SSH登录用户名", max_length=256, help_text="SSH登录用户名") password = models.CharField( "SSH登录密码", max_length=256, help_text="SSH登录密码") data_folder = models.CharField( "数据分区", max_length=256, default="/data", help_text="数据分区") service_num = models.IntegerField( "服务个数", default=0, help_text="服务个数") alert_num = models.IntegerField( "告警次数", default=0, help_text="告警次数") operate_system = models.CharField( "操作系统", max_length=128, help_text="操作系统") host_name = models.CharField( "主机名", max_length=64, blank=True, null=True, help_text="主机名") memory = models.IntegerField( "内存", blank=True, null=True, help_text="内存") cpu = models.IntegerField( "CPU", blank=True, null=True, help_text="CPU") disk = models.JSONField( "磁盘", blank=True, null=True, help_text="磁盘") host_agent = models.CharField( "主机Agent状态", max_length=16, help_text="主机Agent状态", choices=AGENT_STATUS_CHOICES, default=AGENT_DEPLOY_ING) monitor_agent = models.CharField( "监控Agent状态", max_length=16, help_text="监控Agent状态", choices=AGENT_STATUS_CHOICES, default=AGENT_DEPLOY_ING) host_agent_error = models.CharField( "主机Agent异常信息", max_length=256, blank=True, null=True, help_text="主机Agent异常信息") monitor_agent_error = models.CharField( "监控Agent异常信息", max_length=256, blank=True, null=True, help_text="监控Agent异常信息") is_maintenance = models.BooleanField( "维护模式", default=False, help_text="维护模式") agent_dir = models.CharField( "Agent安装目录", max_length=256, default="/data", help_text="Agent安装目录") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="环境", help_text="环境") init_status = models.CharField( "主机初始化状态", max_length=16, help_text="主机初始化状态", choices=INIT_STATUS_CHOICES, default=INIT_NOT_EXECUTED) use_ntpd = models.BooleanField("是否开启时间同步服务", default=False) ntpd_server = models.GenericIPAddressField("时间同步服务器地址", default="127.0.0.1") ntpdate_install_status = models.CharField( "安装ntpdate状态", max_length=16, choices=NTPDATE_STATUS_CHOICES, default=NTPDATE_NOT_INSTALL ) class Meta: """ 元数据 """ db_table = "omp_host" verbose_name = verbose_name_plural = "主机" ordering = ("-created",) class HostOperateLog(models.Model): """ 主机操作记录表 """ objects = None username = models.CharField( "操作用户", max_length=128, help_text="操作用户") description = models.CharField( "用户行为描述", max_length=1024, help_text="用户行为描述") result = models.CharField( "操作结果", max_length=1024, default="success", help_text="操作结果") created = models.DateTimeField( '发生时间', null=True, auto_now_add=True, help_text='发生时间') host = models.ForeignKey( Host, null=True, on_delete=models.SET_NULL, verbose_name="主机") class Meta: """ 元数据 """ db_table = "omp_host_operate_log" verbose_name = verbose_name_plural = "主机操作记录" ordering = ("-created",) ================================================ FILE: omp_server/db_models/models/inspection.py ================================================ import os from django.db import models from .env import Env class InspectionHistory(models.Model): """巡检记录历史表""" objects = None id = models.AutoField(primary_key=True, unique=True, help_text="自增主键") inspection_name = models.CharField( max_length=256, null=False, blank=False, help_text="报告名称:巡检类型名称") inspection_type = models.CharField( max_length=32, default="service", help_text="巡检类型,service、host、deep") inspection_status = models.IntegerField( default=0, help_text="巡检状态:1-进行中;2-成功;3-失败") execute_type = models.CharField( max_length=32, null=False, blank=False, default="man", help_text="执行方式: 手动-man;定时:auto") inspection_operator = models.CharField( max_length=16, null=False, blank=False, help_text="操作人员-当前登录人") hosts = models.JSONField( null=True, blank=True, help_text="巡检主机:['10.0.9.158']") services = models.JSONField( null=True, blank=True, help_text="巡检组件: [8,9]") start_time = models.DateTimeField(auto_now_add=True, help_text="开始时间") end_time = models.DateTimeField(null=True, help_text="结束时间,后端后补") duration = models.IntegerField(default=0, help_text="巡检用时:单位s,后端后补") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="当前环境id", help_text="当前环境id") NOT_SEND = 3 SUCCESS = 1 ING = 2 FAIL = 0 SEND_RESULT_CHOICES = ( ("发送成功", SUCCESS), ("发送中", ING), ("发送失败", FAIL), ("未发送", NOT_SEND) ) send_email_result = models.IntegerField( "邮件推送状态", choices=SEND_RESULT_CHOICES, default=NOT_SEND) class Meta: db_table = 'inspection_history' verbose_name = verbose_name_plural = "巡检记录历史表" ordering = ("-start_time",) def send_email_content(self): return f""" 巡检任务名称:{self.inspection_name}\n 巡检时间:{self.start_time.strftime("%Y-%m-%d %H:%M:%S")} """ def fetch_file_kwargs(self): from omp_server.settings import PROJECT_DIR inspection_report = InspectionReport.objects.filter( inst_id=self.id).first() file_path = os.path.join( PROJECT_DIR, f"data/inspection_file/{inspection_report.file_name}") return {"path": file_path} class InspectionCrontab(models.Model): """巡检任务 定时配置表""" j_type = ( (0, "深度分析"), (1, "主机巡检"), (2, "组件巡检") ) objects = None id = models.AutoField(primary_key=True, unique=True, help_text="自增主键") job_type = models.IntegerField( default=0, choices=j_type, help_text="任务类型:0-深度分析 1-主机巡检 2-组建巡检") job_name = models.CharField( max_length=128, null=False, blank=False, help_text="任务名称") is_start_crontab = models.IntegerField( default=0, help_text="是否开启定时任务:0-开启,1-关闭") crontab_detail = models.JSONField(help_text="定时任务详情") create_date = models.DateTimeField(auto_now_add=True, help_text="创建时间") update_time = models.DateTimeField(auto_now=True, help_text="修改时间") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="环境", help_text="环境") class Meta: """表名等信息""" db_table = 'inspection_crontab' verbose_name = verbose_name_plural = "巡检任务 定时配置表" ordering = ("id",) class InspectionReport(models.Model): """巡检 报告""" objects = None id = models.AutoField(primary_key=True, unique=True, help_text="自增主键") file_name = models.CharField( max_length=128, null=True, blank=True, help_text="导出文件名") scan_info = models.JSONField(null=True, blank=True, help_text="扫描统计") scan_result = models.JSONField(null=True, blank=True, help_text="分析结果") risk_data = models.JSONField(null=True, blank=True, help_text="风险指标") host_data = models.JSONField(null=True, blank=True, help_text="主机列表") serv_plan = models.JSONField(null=True, blank=True, help_text="服务平面图") serv_data = models.JSONField(null=True, blank=True, help_text="服务列表") inst_id = models.OneToOneField( InspectionHistory, null=True, on_delete=models.SET_NULL, verbose_name="巡检记录历史表", help_text="巡检记录历史表id") class Meta: """表名等信息""" db_table = 'inspection_report' verbose_name = verbose_name_plural = "巡检任务 报告数据" ordering = ("id",) ================================================ FILE: omp_server/db_models/models/install.py ================================================ from django.db import models from db_models.mixins import TimeStampMixin from .service import Service class MainInstallHistory(TimeStampMixin): """ 主安装记录表 """ objects = None INSTALL_STATUS_READY = 0 INSTALL_STATUS_INSTALLING = 1 INSTALL_STATUS_SUCCESS = 2 INSTALL_STATUS_FAILED = 3 INSTALL_STATUS_REGISTER = 4 INSTALL_STATUS_CHOICES = ( (INSTALL_STATUS_READY, "待安装"), (INSTALL_STATUS_INSTALLING, "安装中"), (INSTALL_STATUS_SUCCESS, "安装成功"), (INSTALL_STATUS_FAILED, "安装失败"), (INSTALL_STATUS_REGISTER, "正在注册"), ) operator = models.CharField( "操作用户", max_length=32, null=False, blank=False, default="admin", help_text="用户" ) operation_uuid = models.CharField( "部署操作uuid", max_length=36, null=False, blank=False, help_text="部署操作uuid") task_id = models.CharField( "异步任务id", max_length=36, null=True, blank=True, help_text="异步任务id") # 直接代表整体的安装状态 install_status = models.IntegerField( "安装状态", choices=INSTALL_STATUS_CHOICES, default=0, help_text="安装状态") install_args = models.JSONField( "主表安装信息", null=True, blank=True, help_text="主表安装信息") install_log = models.TextField("MAIN安装日志", help_text="MAIN安装日志") class Meta: """元数据""" db_table = "omp_main_install_history" verbose_name = verbose_name_plural = "主安装记录表" @property def execution_record_state(self): # 执行记录使用 return self.install_status def operate_count(self, exclude_service_ids=None): # 安装服务个数, exclude_service_ids删除服务前触发 queryset = self.detailinstallhistory_set.filter( service__isnull=False ).exclude(service__service_split=1) if exclude_service_ids: queryset = queryset.exclude(service_id__in=exclude_service_ids) return queryset.values("service_id").distinct().count() @property def module_id(self): return self.operation_uuid class PreInstallHistory(TimeStampMixin): """ 记录安装过程中主机的操作记录内容 """ objects = None main_install_history = models.ForeignKey( MainInstallHistory, null=True, blank=True, on_delete=models.SET_NULL, help_text="关联主安装记录") name = models.CharField( "名称", max_length=32, blank=False, null=False, default="初始化安装流程", help_text="名称" ) ip = models.GenericIPAddressField( "主机ip地址", blank=False, null=False, help_text="主机ip地址") install_flag = models.IntegerField( "安装标志", default=0, help_text="0-待安装 1-安装中 2-安装成功 3-安装失败") install_log = models.TextField("主机层安装日志", help_text="主机层安装日志") class Meta: """元数据""" db_table = "omp_pre_install_history" verbose_name = verbose_name_plural = "前置安装记录" class PostInstallHistory(TimeStampMixin): """ 记录安装完成后的其他操作,如注册、tengine、nacos更新 """ objects = None main_install_history = models.ForeignKey( MainInstallHistory, null=True, blank=True, on_delete=models.SET_NULL, help_text="关联主安装记录") name = models.CharField( "名称", max_length=32, blank=False, null=False, default="安装后续任务", help_text="名称" ) ip = models.CharField( "fake主机ip地址", blank=False, null=False, default="postAction", max_length=128, help_text="主机ip地址") install_flag = models.IntegerField( "安装标志", default=0, help_text="0-待执行 1-执行中 2-执行成功 3-执行失败") install_log = models.TextField("安装后续任务日志", help_text="安装后续任务日志") class Meta: """元数据""" db_table = "omp_post_install_history" verbose_name = verbose_name_plural = "后置安装记录" class DetailInstallHistory(TimeStampMixin): """ 安装细节表,针对单服务 在下发安装任务之前,需要对安装顺序进行排序确定 """ objects = None INSTALL_STATUS_READY = 0 INSTALL_STATUS_INSTALLING = 1 INSTALL_STATUS_SUCCESS = 2 INSTALL_STATUS_FAILED = 3 INSTALL_STEP_CHOICES = ( (INSTALL_STATUS_READY, "待安装"), (INSTALL_STATUS_INSTALLING, "安装中"), (INSTALL_STATUS_SUCCESS, "安装成功"), (INSTALL_STATUS_FAILED, "安装失败"), ) # 若修改on_delete,需处理update_execution_record service = models.ForeignKey( Service, null=True, blank=True, on_delete=models.SET_NULL, help_text="关联服务对象") main_install_history = models.ForeignKey( MainInstallHistory, null=True, blank=True, on_delete=models.SET_NULL, help_text="关联主安装记录") # 单服务安装步骤: install_step_status = models.IntegerField( "安装步骤状态", choices=INSTALL_STEP_CHOICES, default=0, help_text="安装步骤状态") # 安装细节标记及日志 send_flag = models.IntegerField( "发包状态", default=0, help_text="0-待发送 1-发送中 2-发送成功 3-发送失败") send_msg = models.TextField("发包日志", help_text="发包日志") unzip_flag = models.IntegerField( "解压包状态", default=0, help_text="0-待解压 1-解压中 2-解压成功 3-解压失败") unzip_msg = models.TextField("解压日志", help_text="解压日志") install_flag = models.IntegerField( "安装执行状态", default=0, help_text="0-待安装 1-安装中 2-安装成功 3-安装失败") install_msg = models.TextField("安装日志", help_text="安装日志") init_flag = models.IntegerField( "初始化执行状态", default=0, help_text="0-待初始化 1-初始化中 2-初始化成功 3-初始化失败") init_msg = models.TextField("初始化日志", help_text="初始化日志") start_flag = models.IntegerField( "启动执行状态", default=0, help_text="0-待启动 1-启动中 2-启动成功 3-启动失败") start_msg = models.TextField("启动日志", help_text="启动日志") install_detail_args = models.JSONField( "详情表安装信息", null=True, blank=True, help_text="详情表安装信息") post_action_flag = models.IntegerField( "安装后执行动作标记", default=0, help_text="0-待执行 1-执行中 2-执行成功 3-执行失败 4-无需执行") post_action_msg = models.TextField( "安装后执行动作日志", default="", help_text="安装后执行动作日志") class Meta: """元数据""" db_table = "omp_detail_install_history" verbose_name = verbose_name_plural = "安装记录详情表" def __str__(self): return self.service.service_instance_name + f"({self.service.ip})" class DeploymentPlan(models.Model): """ 部署计划 """ plan_name = models.CharField( "部署计划名称", max_length=32, null=False, blank=False, help_text="部署计划名称") host_num = models.IntegerField( "主机数量", default=0, help_text="主机数量") product_num = models.IntegerField( "产品数量", default=0, help_text="产品数量") service_num = models.IntegerField( "服务数量", default=0, help_text="服务数量") create_user = models.CharField( "创建用户", max_length=16, null=False, blank=False, help_text="创建用户") operation_uuid = models.CharField( "部署操作uuid", max_length=36, null=False, blank=False, help_text="部署操作uuid") created = models.DateTimeField( "创建时间", null=True, auto_now_add=True, help_text="创建时间") class Meta: db_table = 'omp_deployment_plan' verbose_name = verbose_name_plural = '部署计划' ================================================ FILE: omp_server/db_models/models/monitor.py ================================================ from django.db import models from .env import Env class MonitorUrl(models.Model): """ 用户操作记录表 """ objects = None name = models.CharField( "监控类别", max_length=32, unique=True, help_text="监控类别") monitor_url = models.CharField( "请求地址", max_length=128, help_text="请求地址") class Meta: """ 元数据 """ db_table = "omp_promemonitor_url" verbose_name = verbose_name_plural = "监控地址记录" class Alert(models.Model): """告警数据表""" objects = None is_read = models.IntegerField( "已读", default=0, help_text="此消息是否已读,0-未读;1-已读") alert_type = models.CharField( "告警类型", max_length=32, default="", help_text="告警类型,主机host,服务service") alert_host_ip = models.CharField( "告警主机ip", max_length=64, default="", help_text="告警来源主机ip") alert_service_name = models.CharField( "告警服务名称", max_length=128, default="", help_text="服务类告警中的服务名称") alert_instance_name = models.CharField( "告警实例名称", max_length=128, default="", help_text="告警实例名称") alert_service_type = models.CharField( "告警服务类型", max_length=128, default="", help_text="服务所属类型") alert_level = models.CharField( "告警级别", max_length=1024, default="", help_text="告警级别") alert_describe = models.CharField( "告警描述", max_length=1024, default="", help_text="告警描述") alert_receiver = models.CharField( "告警接收人", max_length=256, default="", help_text="告警接收人") alert_resolve = models.CharField( "告警解决方案", max_length=1024, default="", help_text="告警解决方案") alert_time = models.DateTimeField( "告警发生时间", help_text="告警发生时间") create_time = models.DateTimeField( "告警信息入库时间", auto_now_add=True, help_text="告警信息入库时间") monitor_path = models.CharField( "跳转监控路径", max_length=2048, blank=True, null=True, help_text="跳转grafana路由") monitor_log = models.CharField( "跳转监控日志路径", max_length=2048, blank=True, null=True, help_text="跳转grafana日志页面路由") fingerprint = models.CharField( "告警的唯一标识", max_length=1024, blank=True, null=True, help_text="告警的唯一标识") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="环境", help_text="环境") class Meta: """元数据""" db_table = 'omp_alert' verbose_name = verbose_name_plural = "告警记录" class Maintain(models.Model): """ 维护记录表 """ objects = None matcher_name = models.CharField( "匹配标签", max_length=1024, null=False, help_text="匹配标签") matcher_value = models.CharField( "匹配值", max_length=1024, null=False, help_text="匹配值") maintain_id = models.CharField( "维护唯一标识", max_length=1024, null=False, help_text="维护唯一标识") class Meta: """元数据""" db_table = 'omp_maintain' verbose_name = verbose_name_plural = "维护记录" class GrafanaMainPage(models.Model): """Grafana 主面板信息表""" instance_name = models.CharField( "实例名字", max_length=32, unique=True, help_text="信息面板实例名字") instance_url = models.CharField( "实例地址", max_length=255, unique=True, help_text="实例文根地址") class Meta: """ 元数据 """ db_table = "omp_grafana_url" verbose_name = verbose_name_plural = "grafana面板记录" class AlertSendWaySetting(models.Model): used = models.BooleanField("是否启用", default=False) env_id = models.IntegerField("环境id", default=0) way_name = models.CharField("告警推送服务名称", max_length=64) server_url = models.TextField("告警推送服务url", default="") way_token = models.CharField("服务token", max_length=255, default="") extra_info = models.JSONField("服务其他信息", default=dict) class Meta: db_table = 'omp_alert_send_way_setting' verbose_name = verbose_name_plural = '告警推送通道设置' # 暂时屏蔽doem的配置 # @property # def get_doem_init_kwargs(self): # """ # 返回告警推送服务类对象初始化参数 # :return: # """ # return {"app_key": self.way_token, "server_url": self.server_url} # # @property # def get_doem_dict(self): # return { # "way_token": self.way_token, # "server_url": self.server_url, # "used": self.used # } @property def get_email_dict(self): return { "server_url": self.server_url, "used": self.used } @classmethod def get_v1_5_email_dict(cls, env_id): obj = cls.objects.filter(way_name="email").first() kwargs = { "server_url": "", "used": False } if obj: kwargs.update(server_url=obj.server_url, used=obj.used) cls.objects.create( env_id=env_id, way_name="email", server_url=obj.server_url, used=obj.used) return kwargs def get_self_dict(self): # 前端展示 return getattr(self, f"get_{self.way_name}_dict") # def get_func_class_obj(self): # """ # 返回告警推送服务类对象 # :return: # """ # from base import base_monitor # kwargs = getattr(self, f"get_{self.way_name}_init_kwargs") # return getattr( # base_monitor, f"SendAlertTo{self.way_name.capitalize()}Way" # )(**kwargs) @classmethod def update_email_config(cls, used, user_emails): cls.objects.filter( way_name="email" ).update(used=used, server_url=user_emails) ================================================ FILE: omp_server/db_models/models/product.py ================================================ from django.db import models from db_models.mixins import TimeStampMixin, DeleteMixin class Labels(models.Model): """ 应用&产品标签表 """ LABEL_TYPE_COMPONENT = 0 LABEL_TYPE_APPLICATION = 1 LABELS_CHOICES = ( (LABEL_TYPE_COMPONENT, "组件"), (LABEL_TYPE_APPLICATION, "应用") ) label_name = models.CharField( "标签名称", max_length=16, null=False, blank=False, help_text="标签名称") label_type = models.IntegerField( "标签类型", choices=LABELS_CHOICES, default=0, help_text="标签类型") class Meta: """元数据""" db_table = 'omp_labels' verbose_name = verbose_name_plural = "应用产品标签表" class UploadPackageHistory(TimeStampMixin, DeleteMixin): """ 上传安装包记录表,存储产品包及服务包 """ objects = None PACKAGE_STATUS_SUCCESS = 0 PACKAGE_STATUS_FAILED = 1 PACKAGE_STATUS_PARSING = 2 PACKAGE_STATUS_PUBLISH_SUCCESS = 3 PACKAGE_STATUS_PUBLISH_FAILED = 4 PACKAGE_STATUS_PUBLISHING = 5 PACKAGE_STATUS_CHOICES = ( (PACKAGE_STATUS_SUCCESS, "成功"), (PACKAGE_STATUS_FAILED, "失败"), (PACKAGE_STATUS_PARSING, "解析中"), (PACKAGE_STATUS_PUBLISH_SUCCESS, "发布成功"), (PACKAGE_STATUS_PUBLISH_FAILED, "发布失败"), (PACKAGE_STATUS_PUBLISHING, "发布中"), ) operation_uuid = models.CharField( "唯一操作uuid", max_length=64, null=False, blank=False, help_text="唯一操作uuid") operation_user = models.CharField( "操作用户", max_length=64, null=True, blank=True, help_text="操作用户") package_name = models.CharField( "安装包名称", max_length=256, null=False, blank=False, help_text="安装包名称") package_md5 = models.CharField( "安装包MD5值", max_length=64, null=False, blank=False, help_text="安装包MD5值") # 安装包相对路径,相对于package_hub package_path = models.CharField( "安装包路径", max_length=512, null=False, blank=False, help_text="安装包路径") package_status = models.IntegerField( "安装包状态", choices=PACKAGE_STATUS_CHOICES, default=2, help_text="安装包状态") error_msg = models.CharField( "错误消息", max_length=1024, null=True, blank=True, help_text="错误消息") package_parent = models.ForeignKey( to="self", null=True, blank=True, on_delete=models.SET_NULL, help_text="父级包") class Meta: """元数据""" db_table = 'omp_upload_package_history' verbose_name = verbose_name_plural = "上传安装包记录" class ProductHub(TimeStampMixin): """ 存储产品级别模型类 (应用) """ # 使用is_release标识此条数据是否已发布,是否可用 objects = None is_release = models.BooleanField( "是否发布", default=False, help_text="是否发布") pro_name = models.CharField( "产品名称", max_length=256, null=False, blank=False, help_text="产品名称") pro_version = models.CharField( "产品版本", max_length=256, null=False, blank=False, help_text="产品版本") pro_labels = models.ManyToManyField(to=Labels, help_text="所属标签") pro_description = models.CharField( "产品描述", max_length=2048, null=True, blank=True, help_text="产品描述") # 以下字段在入库时使用json.dumps方法处理,读取时使用json.loads方法反向解析 # 产品依赖默认向下兼容 # ["cmdb", "douc"] pro_dependence = models.TextField( "产品依赖", null=True, blank=True, help_text="产品依赖") # [{"name": "cmdbServer", "version": "1.1.0"}] pro_services = models.TextField( "服务列表", null=True, blank=True, help_text="服务列表") # 关联的安装包 pro_package = models.ForeignKey( UploadPackageHistory, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="安装包", help_text="安装包") # 产品图标读取svg数据进行渲染pro_name.svg pro_logo = models.TextField( "产品图标", null=True, blank=True, help_text="产品图标") # 冗余字段,用于存储未定义的其他产品相关数据 extend_fields = models.JSONField( "冗余字段", null=True, blank=True, help_text="冗余字段") class Meta: """元数据""" db_table = 'omp_product' verbose_name = verbose_name_plural = "应用商店产品" class ApplicationHub(TimeStampMixin): """ 服务级别模型类 (组件) """ objects = None APP_TYPE_COMPONENT = 0 APP_TYPE_SERVICE = 1 APP_TYPE_CHOICES = ( (APP_TYPE_COMPONENT, "组件"), (APP_TYPE_SERVICE, "服务") ) is_release = models.BooleanField( "是否发布", default=False, help_text="是否发布") app_type = models.IntegerField( "应用类型", choices=APP_TYPE_CHOICES, default=0, help_text="应用类型") app_name = models.CharField( "应用名称", max_length=256, null=False, blank=False, help_text="应用名称") app_labels = models.ManyToManyField(to=Labels, help_text="所属标签") app_version = models.CharField( "应用版本", max_length=256, null=False, blank=False, help_text="应用版本") app_description = models.CharField( "应用描述", max_length=2048, null=True, blank=True, help_text="应用描述") # 应用端口使用TextField字段进行存储 # 在入库时使用json.dumps方法处理,读取时使用json.loads方法反向解析 # 存储数据格式为[{"default": 18080, "key": "http_port", "name": "服务端口"}] app_port = models.TextField( "应用端口", null=True, blank=True, help_text="应用端口") # 以下字段使用方法同应用端口 # 服务依赖默认向下兼容 # [{"name": "mysql", "version": "5.7"}] app_dependence = models.TextField( "应用依赖", null=True, blank=True, help_text="应用依赖") # [{"name": "安装目录", "key": "base_dir", "default": "{data_path}/abc"}] app_install_args = models.TextField( "安装参数", null=True, blank=True, help_text="安装参数") # {"start": "./start.sh", "stop": "./stop.sh"} app_controllers = models.TextField( "应用控制脚本", null=True, blank=True, help_text="应用控制脚本") # 关联的安装包 app_package = models.ForeignKey( UploadPackageHistory, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="安装包", help_text="安装包") product = models.ForeignKey( ProductHub, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="所属产品", help_text="所属产品") # 应用图标读取svg数据进行渲染app_name.svg app_logo = models.TextField( "应用图标", null=True, blank=True, help_text="应用图标") # 冗余字段,用于存储未定义的其他服务相关数据 extend_fields = models.JSONField( "冗余字段", null=True, blank=True, help_text="冗余字段") # 解析服务数据后,如果服务为jdk、python等,则其为基础数据 is_base_env = models.BooleanField( "是否为基础环境", default=False, help_text="是否为基础环境") app_monitor = models.JSONField( "监控使用字段", null=True, blank=True, help_text="监控使用字段") class Meta: """元数据""" db_table = 'omp_application' verbose_name = verbose_name_plural = "应用商店服务" # 服务、组件名称和版本形成联合唯一索引,不允许重复 unique_together = ("app_name", "app_version") @property def pro_info(self): """ 查询是product名字 """ if self.product: return {"pro_name": self.product.pro_name, "pro_version": self.product.pro_version} return None class Product(TimeStampMixin): """ 已安装产品表 """ objects = None # 用于存储安装产品时使用的实例名称 product_instance_name = models.CharField( "产品实例名称", max_length=64, null=True, blank=True, help_text="安装产品时输入的实例名称") # 所属产品的相关信息,可通过此外键查看其对应的产品仓库中的数据 product = models.ForeignKey( ProductHub, null=True, blank=True, on_delete=models.SET_NULL, help_text="所属产品") class Meta: """元数据""" db_table = 'omp_product_instance' verbose_name = verbose_name_plural = "产品实例表" ordering = ("-created",) ================================================ FILE: omp_server/db_models/models/self_heal.py ================================================ from django.db import models from django.core.validators import MaxValueValidator class WaitSelfHealing(models.Model): # 0 等待自愈 1 自愈中 存在自愈中的服务不被允许再次触发自愈,保证自愈过程中只有一个自愈在进行。 service_name = models.CharField("自愈服务名称", max_length=64, default="") repair_ser = models.JSONField("自愈缓存服务详情", default=dict) repair_status = models.IntegerField("自愈缓存服务", default=0) class Meta: db_table = "omp_wait_self_healing" verbose_name = verbose_name_plural = "自愈等待队列" class SelfHealingSetting(models.Model): """自愈策略设置表""" REPAIR_CHOICES = [ (0, "启动"), (1, "重新启动") ] used = models.BooleanField("是否启用", default=False) max_healing_count = models.IntegerField("最多自愈操作次数", default=5, validators=[MaxValueValidator(20)]) fresh_rate = models.IntegerField("周期内采集告警消息频次", default=10, validators=[MaxValueValidator(60)]) instance_tp = models.IntegerField("实例类别", choices=REPAIR_CHOICES, default=0) repair_instance = models.JSONField("修复实例", default=dict, blank=False, null=False) class Meta: db_table = "omp_self_healing_setting" verbose_name = verbose_name_plural = "自愈设置" class SelfHealingHistory(models.Model): """自愈历史记录""" is_read = models.IntegerField("此消息是否已读,0-未读;1-已读", default=0) host_ip = models.CharField("自愈主机ip", max_length=64) service_name = models.CharField("自愈服务名称", max_length=64, default="") instance_name = models.CharField("自愈实例名称", max_length=128, default="", help_text="自愈实例名称") HEALING_FAIL = 0 HEALING_ING = 2 HEALING_SUCCESS = 1 STATE_CHOICES = ( (HEALING_ING, "自愈中"), (HEALING_FAIL, "自愈失败"), (HEALING_SUCCESS, "自愈成功") ) state = models.IntegerField("自愈状态", choices=STATE_CHOICES, default=HEALING_ING) healing_count = models.IntegerField("已运行自愈次数", default=0) start_time = models.DateTimeField("自愈开始时间", null=True) end_time = models.DateTimeField("自愈结束时间", null=True) healing_log = models.TextField("自愈日志", default="") alert_time = models.DateTimeField("关联告警时间,与fingerprint确定同一次告警", null=True) alert_content = models.TextField("告警日志内容", default="") monitor_log = models.TextField("grafana日志url", default="") class Meta: db_table = "omp_self_healing_history" verbose_name = verbose_name_plural = "自愈历史记录" ================================================ FILE: omp_server/db_models/models/service.py ================================================ import json import os from django.db import models from db_models.mixins import TimeStampMixin, DeleteMixin from utils.common.exceptions import GeneralError from .env import Env from .product import ApplicationHub class ServiceConnectInfo(TimeStampMixin): """ 存储用户名密码信息 """ # 服务用户名、密码信息,同一个集群公用一套用户名、密码 objects = None service_name = models.CharField( "服务名", max_length=32, null=False, blank=False, help_text="服务名") service_username = models.CharField( "用户名", max_length=512, null=True, blank=True, help_text="用户名") service_password = models.CharField( "密码", max_length=512, null=True, blank=True, help_text="密码") service_username_enc = models.CharField( "加密用户名", max_length=512, null=True, blank=True, help_text="加密用户名") service_password_enc = models.CharField( "加密密码", max_length=512, null=True, blank=True, help_text="加密密码") class Meta: """ 元数据 """ db_table = "omp_service_connect_info" verbose_name = verbose_name_plural = "用户名密码信息表" class ClusterInfo(TimeStampMixin, DeleteMixin): """ 集群信息表 """ objects = None cluster_service_name = models.CharField( "集群所属服务", max_length=36, null=True, blank=True, help_text="集群所属服务") # 选择的集群类型 cluster_type = models.CharField( "集群类型", max_length=36, null=True, blank=True, help_text="集群类型") # 集群实例名称,虚拟名称 cluster_name = models.CharField( "集群名称", max_length=64, null=False, blank=False, help_text="集群名称") # 集群连接的信息,公共组件可能存在集群信息,自研服务一般无集群概念 connect_info = models.CharField( "集群连接信息", max_length=512, null=True, blank=True, help_text="集群连接信息") service_connect_info = models.ForeignKey( ServiceConnectInfo, null=True, blank=True, on_delete=models.SET_NULL, help_text="用户名密码信息") # 预留解析字段,集群连接有多种方式 # eg1: 10.0.0.1:18117,10.0.0.2:18117,10.0.0.3:18117 # eg2: 10.0.0.1,10.0.0.2,10.0.0.3:18117 # 在安装时应该按照指定的方式拼接集群信息 connect_info_parse_type = models.CharField( "连接信息解析方式", max_length=32, null=True, blank=True, help_text="连接信息解析方式") class Meta: """元数据""" db_table = "omp_cluster_info" verbose_name = verbose_name_plural = "集群信息表" def __str__(self): return f" of {self.cluster_name}" class ExcludeSplit(models.Manager): """ 拆分前服务过滤,展示监控纳管专用 """ def get_queryset(self): return super(ExcludeSplit, self).get_queryset().exclude(service_split=1) class AfterSplit(models.Manager): """ 拆分后服务过滤,安装升级专用 """ def get_queryset(self): return super(AfterSplit, self).get_queryset().exclude(service_split=2) class AllManage(models.Manager): def get_queryset(self): return super(AllManage, self).get_queryset() class Service(TimeStampMixin): """ 服务表 (删除前会触发update_execution_record)""" split_objects = AfterSplit() objects = ExcludeSplit() all_objects = AllManage() SERVICE_STATUS_NORMAL = 0 SERVICE_STATUS_STARTING = 1 SERVICE_STATUS_STOPPING = 2 SERVICE_STATUS_RESTARTING = 3 SERVICE_STATUS_STOP = 4 SERVICE_STATUS_UNKNOWN = 5 SERVICE_STATUS_INSTALLING = 6 SERVICE_STATUS_INSTALL_FAILED = 7 SERVICE_STATUS_READY = 8 SERVICE_STATUS_DELETING = 9 SERVICE_STATUS_UPGRADE = 10 SERVICE_STATUS_ROLLBACK = 11 SERVICE_STATUS_CHOICES = ( (SERVICE_STATUS_NORMAL, "正常"), (SERVICE_STATUS_STARTING, "启动中"), (SERVICE_STATUS_STOPPING, "停止中"), (SERVICE_STATUS_RESTARTING, "重启中"), (SERVICE_STATUS_STOP, "停止"), (SERVICE_STATUS_UNKNOWN, "未知"), (SERVICE_STATUS_INSTALLING, "安装中"), (SERVICE_STATUS_INSTALL_FAILED, "安装失败"), (SERVICE_STATUS_READY, "待安装"), (SERVICE_STATUS_DELETING, "删除中"), (SERVICE_STATUS_UPGRADE, "升级中"), (SERVICE_STATUS_ROLLBACK, "回滚中") ) PRE_IS_SPLIT = 1 NO_SPLIT = 0 AFT_IS_SPLIT = 2 SERVICE_STATUS_SPLIT = ( ( (PRE_IS_SPLIT, "拆分前"), (AFT_IS_SPLIT, "拆分后"), (NO_SPLIT, "未拆分") ) ) # 是否用外键关联? ip = models.GenericIPAddressField( "服务所在主机", help_text="服务所在主机") service_instance_name = models.CharField( "服务实例名称", max_length=64, null=False, blank=False, help_text="服务实例名称") service_split = models.IntegerField( "拆分服务前对象", choices=SERVICE_STATUS_SPLIT, default=0, help_text="拆分服务前对象") service = models.ForeignKey( ApplicationHub, null=True, blank=True, on_delete=models.SET_NULL, help_text="服务表外键") # 以下字段含义同 ApplicationHub 但具备定制化场景,无法做得到唯一关联 # 存储格式[{"port": 18080, "key": "http_port"}] service_port = models.TextField( "服务端口", null=True, blank=True, help_text="服务端口") # 服务控制脚本,按照其所安装的主机拼接绝对路径并进行存储(主机数据目录存在被更改风险) # {"start": "./start.sh", "stop": "./stop.sh"} service_controllers = models.JSONField( "服务控制脚本", null=True, blank=True, help_text="服务控制脚本") # 以下字段用于角色及集群使用 service_role = models.CharField( "服务角色", max_length=128, null=True, blank=True, help_text="服务角色") cluster = models.ForeignKey( ClusterInfo, null=True, blank=True, on_delete=models.SET_NULL, help_text="所属集群") env = models.ForeignKey( Env, null=True, blank=True, on_delete=models.SET_NULL, help_text="所属环境") service_connect_info = models.ForeignKey( ServiceConnectInfo, null=True, blank=True, on_delete=models.SET_NULL, help_text="用户名密码信息") # 服务状态信息 service_status = models.IntegerField( "服务状态", choices=SERVICE_STATUS_CHOICES, default=5, help_text="服务状态") # 服务告警、自愈、监控信息 alert_count = models.IntegerField( "告警次数", default=0, help_text="告警次数") self_healing_count = models.IntegerField( "服务自愈次数", default=0, help_text="服务自愈次数") # 服务实例间的依赖关系,此字段标明当前服务实例所依赖的其他服务实例关系 # 注意,当被依赖服务为base_env时,不在此字段中体现 # 使用json.dumps存储 list结构数据 service_dependence = models.TextField( "服务依赖关系", null=True, blank=True, help_text="服务依赖关系") vip = models.GenericIPAddressField( "vip地址", null=True, blank=True, default=None, help_text="vip地址") class Meta: """元数据""" db_table = 'omp_service' verbose_name = verbose_name_plural = "服务实例表" ordering = ("-created",) def update_port(self, app_ports): """ 比较服务端口,取并集 :param app_ports: 目标app服务端口 :return: new service_ports """ # 存储数据格式为[{"default": 18080, "key": "http_port", "name": "服务端口"}] if not app_ports: return [] port_dict = {} service_ports = json.loads(self.service_port or []) for service_port in service_ports: port_dict[service_port.get("key")] = service_port.get("default") for app_port in app_ports: if app_port.get("key") not in port_dict: service_ports.append(app_port) return service_ports def update_controllers(self, application, install_folder): """ 更新服务管理命令 :param application: 服务目标app :param install_folder: 安装目录(用于更新服务命令) :return: new service_controllers """ _app_controllers = json.loads(application.app_controllers) # 获取服务家目录 install_args = json.loads(application.app_install_args) _home = "" for el in install_args: if "dir_key" in el and el["key"] == "base_dir": _home = el["default"] real_home = os.path.join(install_folder, _home.rstrip("/")) _new_controller = dict() # 更改服务控制脚本、拼接相对路径 for key, value in _app_controllers.items(): if not value: continue # 对于hadoop管控命令不动 if application.app_name == "hadoop" and key in \ {"start", "stop", "restart"}: _new_controller[key] = self.service_controllers.get(key) continue _new_controller[key] = os.path.join(real_home, value) # 在每次安装完所有服务后,需要搜索出相应的post_action并统一执行 if "post_action" in application.extend_fields and \ application.extend_fields.get("post_action"): _new_controller["post_action"] = os.path.join( real_home, application.extend_fields["post_action"] ) return _new_controller def update_service_connect_info(self): """ 更新服务链接信息 """ infos = {"username", "password", "username_enc", "password_enc"} connect_infos = {} for app_info in json.loads(self.service.app_install_args): key = app_info.get("key") if key in infos: connect_infos[f"service_{key}"] = app_info.get("default") if not connect_infos: return if self.service_connect_info: for k, v in connect_infos.items(): if not getattr(self.service_connect_info, k) and v: setattr(self.service_connect_info, k, v) self.service_connect_info.save() return conn_obj, _ = ServiceConnectInfo.objects.get_or_create( service_name=self.service.app_name, **connect_infos ) self.service_connect_info = conn_obj @classmethod def update_dependence(cls, service_dependence, app_dependence): # 暂不考虑服务依赖减少 if not app_dependence: return [] dependence_dict = {} dependents = json.loads(service_dependence or '[]') for dependence in dependents: dependence_dict[dependence.get("name")] = dependence for _dependence in app_dependence: service_name = _dependence.get("name") if service_name not in dependence_dict: service = Service.objects.filter( service__app_name=service_name ).first() if not service: raise GeneralError(f"缺少依赖服务{service_name}!") _dict = { "name": service_name, "cluster_name": None, "instance_name": None } if service.cluster: _dict["cluster_name"] = service.cluster.cluster_name else: _dict["instance_name"] = service.service_instance_name dependents.append(_dict) return dependents def update_application(self, application, success, install_folder): """ 更新服务信息 :param application: 服务目标app :param success: 是否成功(用于更新服务状态) :param install_folder: 安装目录(用于更新服务命令) :return: self """ self.service = application self.service_port = json.dumps( self.update_port(json.loads(application.app_port or '[]')) ) self.service_dependence = json.dumps( self.update_dependence( self.service_dependence, json.loads(application.app_dependence or '[]') ) ) self.service_controllers = self.update_controllers( application, install_folder) if success: self.service_status = self.SERVICE_STATUS_NORMAL else: self.service_status = self.SERVICE_STATUS_UNKNOWN self.update_service_connect_info() self.save() return self class ServiceHistory(models.Model): """ 服务操作记录表 """ objects = None username = models.CharField( "操作用户", max_length=128, help_text="操作用户") description = models.CharField( "用户行为描述", max_length=1024, help_text="用户行为描述") # success or failed result = models.CharField( "操作结果", max_length=1024, default="success", help_text="操作结果") created = models.DateTimeField( '发生时间', null=True, auto_now_add=True, help_text='发生时间') service = models.ForeignKey( Service, null=True, on_delete=models.SET_NULL, verbose_name="服务") class Meta: """ 元数据 """ db_table = "omp_service_operate_log" verbose_name = verbose_name_plural = "服务操作记录" ordering = ("-created",) @classmethod def create_history(cls, service, operation_obj=None, **kwargs): """ 创建服务操作记录 :param service: 被操作的服务 :param operation_obj: 操作对象:UpgradeDetail、RollbackDetail :param kwargs: 记录参数,在无操作对象情况下传 :return: obj """ if operation_obj: operation_kwargs = operation_obj.get_service_history() kwargs.update(operation_kwargs) service_history = cls.objects.create( service=service, **kwargs ) return service_history ================================================ FILE: omp_server/db_models/models/threshold.py ================================================ import hashlib from uuid import uuid4 from django.db import models class HostThreshold(models.Model): """主机阈值设置表""" # 主机指标项名称:cpu_used;memory_used;disk_root_used;disk_data_used index_type = models.CharField( max_length=64, null=False, blank=False, help_text="指标项名称") condition = models.CharField( max_length=32, null=False, blank=False, help_text="阈值条件") condition_value = models.CharField( max_length=128, null=False, blank=False, help_text="阈值数值,百分号格式") # 告警级别: warning critical alert_level = models.CharField( max_length=32, null=False, blank=False, help_text="告警级别") create_date = models.DateTimeField(auto_now_add=True) env_id = models.IntegerField(help_text="环境id", default=1) class Meta: db_table = 'omp_host_threshold' def get_dic(self): return { "index_type": self.index_type, "condition": self.condition, "value": self.condition_value, "level": self.alert_level, } def __str__(self): return str(self.index_type) + "-" + str(self.condition) + "-" \ + str(self.condition_value) + "-" + str(self.alert_level) class ServiceThreshold(models.Model): """服务阈值设置表""" # 服务指标项名称:service_active;service_cpu_used;service_memory_used index_type = models.CharField( "指标项名称", max_length=64, null=False, blank=False) condition = models.CharField( "阈值条件", max_length=32, null=False, blank=False) condition_value = models.CharField( "阈值数值,百分号格式", max_length=128, null=False, blank=False) # 告警级别: warning critical alert_level = models.CharField( "告警级别", max_length=32, null=False, blank=False) create_date = models.DateTimeField(auto_now_add=True) env_id = models.IntegerField("环境id", default=1) class Meta: db_table = 'omp_service_threshold' def get_dic(self): return { "index_type": self.index_type, "condition": self.condition, "value": self.condition_value, "level": self.alert_level, } def __str__(self): return str(self.index_type) + "-" + str(self.condition) + "-" + \ str(self.condition_value) + "-" + str(self.alert_level) class ServiceCustomThreshold(models.Model): """服务特殊指标阈值设置(临时)""" service_name = models.CharField( "服务名称", max_length=64, null=False, blank=False) index_type = models.CharField( "指标项名称", max_length=64, null=False, blank=False) condition = models.CharField( "阈值条件", max_length=32, null=False, blank=False) condition_value = models.CharField( "阈值数值", max_length=128, null=False, blank=False) # 告警级别: warning critical alert_level = models.CharField( "告警级别", max_length=32, null=False, blank=False) create_date = models.DateTimeField(auto_now_add=True) env_id = models.IntegerField("环境id", default=1) class Meta: db_table = 'omp_service_custom_threshold' verbose_name = verbose_name_plural = '服务定制指标阈值设置' class Rule(models.Model): """ 表达式存储 """ name = models.CharField("指标名称", max_length=255,null=False) expr = models.TextField("监控指标表达式,报警语法", null=False, blank=False) service = models.CharField("服务名称",max_length=255, null=False) description = models.TextField("描述",null=True) class Meta: """ 元数据 """ db_table = "omp_rule" verbose_name = verbose_name_plural = "规则表达式" class AlertRule(models.Model): """ 告警规则 """ env_id = models.IntegerField("环境id", default=1) expr = models.TextField("监控指标表达式,报警语法", null=False, blank=False) threshold_value = models.FloatField("阈值的数值", null=False, blank=False) compare_str = models.CharField("比较符", max_length=64) for_time = models.CharField("持续一段时间获取不到信息就触发告警", max_length=64) severity = models.CharField("告警级别", max_length=64) alert = models.TextField("标题,自定义摘要") service = models.CharField("指标所属服务名称", max_length=255) status = models.IntegerField("启用状态", default=0) name = models.CharField("内置指标名称", max_length=255, null=True) TYPE = ( (0, "builtins"), (1, "custom"), (2, "log") ) quota_type = models.IntegerField("指标的类型", choices=TYPE, default=0) labels = models.JSONField("额外指定标签",null=True) # builtins_not_enabled = models.IntegerField("内置未启用的规则", default=0) summary = models.TextField("描述, 告警指标描述", null=True) description = models.TextField("描述, 告警指标描述", null=True) create_time = models.DateTimeField("告警规则入库时间", auto_now_add=True) update_time = models.DateTimeField("告警规则更新时间", auto_now_add=True) forbidden = models.IntegerField("禁止删除", default=1) # 1能删除 2 禁止删除 hash_data = models.CharField("唯一hash值", null=True, blank=True, max_length=255) # 唯一hash禁止规则重复 class Meta: """ 元数据 """ db_table = "omp_alert_ruler" verbose_name = verbose_name_plural = "自定义告警规则" ================================================ FILE: omp_server/db_models/models/tool.py ================================================ # -*- coding: utf-8 -*- # Project: tools # Author: jon.liu@yunzhihui.com # Create time: 2022-02-08 16:04 # IDE: PyCharm # Version: 1.0 # Introduction: """ 实用工具数据库表结构 原始小工具校验前地址:omp/package_hub/tool/verify_tar/ 原始小工具tar包地址:omp/package_hub/tool/tar/ 小工具解压后包地址: omp/package_hub/tool/folder/ 上传的文件地址: omp/package_hub/tool/upload_data/ 运行产生的的文件地址: omp/package_hub/tool/download_data/ """ import os from django.conf import settings from django.db import models from db_models.mixins import TimeStampMixin from utils.plugin.public_utils import timedelta_strftime from db_models.models import Host class ToolInfo(TimeStampMixin): """ 实用工具基本信息记录表 """ objects = None KIND_MANAGEMENT = 0 KIND_CHECK = 1 KIND_SECURITY = 2 KIND_OTHER = 3 TOOL_KIND_CHOICES = ( (KIND_MANAGEMENT, "管理工具"), (KIND_CHECK, "检查工具"), (KIND_SECURITY, "安全工具"), (KIND_OTHER, "其他工具") ) SCRIPT_TYPE_PYTHON3 = 0 SCRIPT_TYPE_SHELL = 1 SCRIPT_TYPE_CHOICES = ( (SCRIPT_TYPE_PYTHON3, "python3"), (SCRIPT_TYPE_SHELL, "shell") ) OUTPUT_TERMINAL = 0 OUTPUT_FILE = 1 OUTPUT_TYPE_CHOICES = ( (OUTPUT_TERMINAL, "终端输出"), (OUTPUT_FILE, "文件输出") ) name = models.CharField( max_length=128, null=False, blank=False, help_text="实用工具名称") kind = models.IntegerField( "实用工具分类", choices=TOOL_KIND_CHOICES, default=0, help_text="实用工具分类") script_type = models.IntegerField( "脚本类型", choices=SCRIPT_TYPE_CHOICES, default=0, help_text="脚本类型") # 脚本执行的目标对象,主机为host,服务为服务名称 target_name = models.CharField( "脚本执行的目标对象", max_length=128, default='host', help_text="脚本执行的目标对象") # 原始脚本包MD5值,预留字段 source_package_md5 = models.CharField( "源码包md5值", max_length=32, blank=True, null=True, help_text="源码包md5值") # 原始tar包相对路径,package_hub/{tool/tar/kafka_tool.tar.gz} source_package_path = models.CharField( "源码包相对路径", max_length=128, null=False) # 存储实用工具目录路径,如package_hub/{tool/folder/kafka-package_md5} tool_folder_path = models.CharField( "实用工具目录相对路径", max_length=128, null=False, blank=False, help_text="实用工具目录相对路径") # 存储脚本路径,如kafka.py script_path = models.CharField( "脚本相对路径", max_length=128, null=False, blank=False, help_text="脚本相对路径") send_package = models.JSONField( "需要发送的文件相对路径", max_length=128, default=list, help_text="需要发送的文件相对路径") # 存储readme的内容 readme_info = models.TextField( "readme信息", null=True, blank=True, help_text="readme信息") # 如果脚本需要模板文件,那么该模板文件的相对路径需要存储到下面字段中 # 此字段存储列表类型数据 template_filepath = models.JSONField( "模板文件相对路径", default=list, help_text="模板文件相对路径") # 在执行对象为服务时需要获取除ServiceConnectInfo中以外的信息 # ["service_port", "metrics_port"] obj_connection_args = models.JSONField("目标对象连接信息", default=list, ) # 存储脚本执行参数,存储列表类型数据 # 在入库时需要对每个参数的类型进行校验(前端展示效果) script_args = models.JSONField("脚本执行参数", default=list) # 脚本输出的类型,终端/文件 output = models.IntegerField( "脚本的输出类型", choices=OUTPUT_TYPE_CHOICES, default=0, help_text="脚本的输出类型") description = models.TextField("描述信息", help_text="描述信息") logo = models.URLField("logo url", default="") class Meta: """元数据""" db_table = "omp_tool_info" verbose_name = verbose_name_plural = "实用工具基本信息表" def load_default_form(self): return { "task_name": self.name, "target_objs": [], 'runuser': '', 'timeout': 60 } # 目前支持的参数类型("select_multiple":多选暂不支持) # "select":单选, "file":文件, "input":单行文本 @property def package_name(self): return self.tool_folder_path.split("/")[-1] @property def templates(self): templates = [] for template in self.template_filepath: templates.append( { "name": template.split("/")[-1], "sub_url": os.path.join(self.tool_folder_path, template) } ) return templates @property def tar_url(self): return os.path.join(self.source_package_path) def valid_upload_file(self, *args, **kwargs): return f"package_hub/tool/upload_data/{self.package_name}" def upload_file_url(self, file_name, **kwargs): return os.path.join(f"tool/upload_data/{self.package_name}", file_name) class ToolExecuteMainHistory(models.Model): """ 实用工具执行记录 """ objects = None STATUS_READY = 0 STATUS_RUNNING = 1 STATUS_SUCCESS = 2 STATUS_FAILED = 3 STATUS_TYPE_CHOICES = ( (STATUS_READY, "待执行"), (STATUS_RUNNING, "执行中"), (STATUS_SUCCESS, "执行成功"), (STATUS_FAILED, "执行失败"), ) tool = models.ForeignKey( ToolInfo, on_delete=models.CASCADE, help_text="实用工具对象") task_name = models.CharField( "任务标题", max_length=128, null=True, help_text="任务标题") operator = models.CharField( "操作人", max_length=128, null=True, blank=True, help_text="操作人") status = models.IntegerField( "main执行状态", choices=STATUS_TYPE_CHOICES, default=0, help_text="main执行状态") start_time = models.DateTimeField( "开始时间", null=True, auto_now_add=True, help_text="开始时间") end_time = models.DateTimeField("结束时间", null=True, help_text="结束时间") form_answer = models.JSONField("任务表单提交结果", default=dict) class Meta: """元数据""" db_table = "omp_tool_execute_main_history" verbose_name = verbose_name_plural = "实用工具执行记录" @property def duration(self): if not all([self.end_time, self.start_time]): return "-" return timedelta_strftime(self.end_time - self.start_time) def get_input_files(self): files = [] for answer in self.form_answer.get("script_args", {}): if answer.get("type") == "file" and answer.get("default"): files.append(answer.get("default").get("file_url")) return files class ToolExecuteDetailHistory(TimeStampMixin): """ 实用工具执行详情记录 """ objects = None STATUS_READY = 0 STATUS_RUNNING = 1 STATUS_SUCCESS = 2 STATUS_FAILED = 3 STATUS_TIMEOUT = 4 STATUS_TYPE_CHOICES = ( (STATUS_READY, "待执行"), (STATUS_RUNNING, "执行中"), (STATUS_SUCCESS, "执行成功"), (STATUS_FAILED, "执行失败"), (STATUS_TIMEOUT, "执行超时"), ) main_history = models.ForeignKey( ToolExecuteMainHistory, on_delete=models.CASCADE, help_text="实用工具对象") # 脚本需要在某台主机上执行,此处代表该主机的ip地址 target_ip = models.CharField( "目标IP地址", max_length=64, null=False, blank=False, help_text="目标IP地址") time_out = models.IntegerField("超时时间", default=60) run_user = models.CharField("执行用户", max_length=64, default="") status = models.IntegerField( "detail执行状态", choices=STATUS_TYPE_CHOICES, default=0, help_text="detail执行状态") # 脚本执行的参数详情信息 execute_args = models.JSONField( "执行参数信息", default=dict, help_text="执行参数信息") execute_log = models.TextField("执行日志", help_text="执行日志") # 脚本有输出时使用,{"message": "", "file": ["a.txt", "b.log"]} # 执行脚本有返回写message,有receive_files写file,只写文件名 output = models.JSONField("脚本输出内容", default=dict) class Meta: """元数据""" db_table = "omp_tool_execute_detail_history" verbose_name = verbose_name_plural = "实用工具执行详情表" @property def get_data_dir(self): if hasattr(self, "data_dir"): return self.data_dir data_obj = Host.objects.filter(ip=self.target_ip).first() data_dir = data_obj.data_folder if data_obj else "/tmp" setattr(self, "data_dir", data_dir) return data_dir @property def get_tools_dir(self): if hasattr(self, "tools_dir"): return self.tools_dir tool = self.main_history.tool tools_dir = { "tool_folder_path": tool.tool_folder_path, "script_path": tool.script_path, "send_package": tool.send_package } setattr(self, "tools_dir", tools_dir) return tools_dir def get_cmd_str(self): """ 获取执行命令 :return: 命令字符串 """ # 自定义不支持 interpret_dir = os.path.join( self.get_data_dir, "omp_salt_agent/env/bin/python3") tool_dir = self.get_tools_dir scripts_dir = os.path.join( self.get_data_dir, f"omp_packages/{tool_dir.get('tool_folder_path', '')}", tool_dir.get("script_path") ) interpret_dir = interpret_dir + " " + scripts_dir for key, value in self.execute_args.items(): if value: interpret_dir += " --{0} {1}".format(key, value) return interpret_dir def get_send_files(self): """ 获取需要发送的文件 :return: local_files:需要发送的文件,send_to:发送的位置 """ local_files = self.main_history.get_input_files() tool_dir = self.get_tools_dir tool_folder_path = tool_dir.get("tool_folder_path") local_files.append( os.path.join(tool_folder_path, tool_dir.get("script_path")) ) for file in tool_dir.get("send_package", []): local_files.append(os.path.join(tool_folder_path, file)) return [ { "local_file": local_file, "remote_file": os.path.join( self.get_data_dir, "omp_packages", local_file ) } for local_file in local_files ] def get_receive_files(self): """ 获取需要接受的文件 :return: output_files:需要接受的文件,receive_to:接收文件的存放位置 """ output = self.execute_args.get("output", "").split(",") return { "output_files": output if len(output[0]) != 0 else [], "receive_to": os.path.join( settings.PROJECT_DIR, "package_hub/tool/download_data/" ) } ================================================ FILE: omp_server/db_models/models/upgrade.py ================================================ from django.db import models from .product import ApplicationHub from .env import Env from .service import Service from .user import UserProfile from db_models.mixins import TimeStampMixin, UpgradeStateMixin, \ UpgradeStateChoices, RollBackStateMixin class UpgradeHistory(UpgradeStateMixin, TimeStampMixin): COMMON_OPERATION = [ {"name": "更新data.json", "key": "update_data_json", "result": False} ] operator = models.ForeignKey( UserProfile, null=True, on_delete=models.SET_NULL, verbose_name="用户") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="所属环境") pre_upgrade_state = models.IntegerField( "升级前置结果", choices=UpgradeStateChoices.choices, default=UpgradeStateChoices.UPGRADE_WAIT ) pre_upgrade_result = models.JSONField("升级前置信息", default=dict) class Meta: db_table = "omp_upgrade_history" verbose_name = verbose_name_plural = '升级历史记录' @property def can_roll_back(self): return self.upgradedetail_set.filter( upgrade_state__in=[ UpgradeStateChoices.UPGRADE_SUCCESS, UpgradeStateChoices.UPGRADE_FAIL ], has_rollback=False ).exists() @property def execution_record_state(self): # 执行记录使用 return self.upgrade_state def operate_count(self, exclude_service_ids=None): # 升级服务个数 queryset = self.upgradedetail_set.filter(service__isnull=False) if exclude_service_ids: queryset = queryset.exclude( service_id__in=exclude_service_ids ) return queryset.count() @property def module_id(self): return str(self.id) class UpgradeDetail(UpgradeStateMixin, TimeStampMixin): history = models.ForeignKey( UpgradeHistory, on_delete=models.SET_NULL, null=True, verbose_name="升级历史记录") union_server = models.CharField( "唯一服务实例:ip-app_name(适配hadoop,单服务裂开多个服务)", max_length=199, null=False ) # null=True待定 service = models.ForeignKey( Service, null=True, on_delete=models.SET_NULL, verbose_name="服务") target_app = models.ForeignKey( ApplicationHub, null=True, related_name="target_app_set", on_delete=models.SET_NULL, verbose_name="升级目标服务") current_app = models.ForeignKey( ApplicationHub, null=True, related_name="current_app_set", on_delete=models.SET_NULL, verbose_name="升级前服务") path_info = models.JSONField("服务包备份路径信息", default=dict) handler_info = models.JSONField("升级步骤信息", default=dict) message = models.TextField("升级日志信息", default="") has_rollback = models.BooleanField("是否已回滚", default=False) class Meta: db_table = "omp_upgrade_detail" verbose_name = verbose_name_plural = '单个服务升级记录' def get_service_history(self): """ 组织服务操作记录参数 :return: dict """ return { "username": self.history.operator.username, "description": f"服务自版本{self.current_app.app_version}" f"升级至版本{self.target_app.app_version}", "result": "success" if self.upgrade_state == 2 else "failed", "created": self.created } class RollbackHistory(RollBackStateMixin, TimeStampMixin): operator = models.ForeignKey( UserProfile, null=True, on_delete=models.SET_NULL, verbose_name="用户") env = models.ForeignKey( Env, null=True, on_delete=models.SET_NULL, verbose_name="所属环境") class Meta: db_table = "omp_rollback_history" verbose_name = verbose_name_plural = '回滚历史记录' @property def execution_record_state(self): # 执行记录使用 return self.rollback_state def operate_count(self, exclude_service_ids=None): # 升级服务个数 queryset = self.rollbackdetail_set.filter( upgrade__service__isnull=False ) if exclude_service_ids: queryset = queryset.exclude( upgrade__service_id__in=exclude_service_ids ) return queryset.count() @property def module_id(self): return str(self.id) class RollbackDetail(RollBackStateMixin, TimeStampMixin): history = models.ForeignKey( RollbackHistory, on_delete=models.SET_NULL, null=True, verbose_name="回滚历史记录") upgrade = models.ForeignKey( UpgradeDetail, null=True, on_delete=models.SET_NULL, verbose_name="升级记录") path_info = models.JSONField("服务包备份路径信息", default=dict) handler_info = models.JSONField("回滚步骤信息", default=dict) message = models.TextField("回滚日志信息", default="") class Meta: db_table = "omp_rollback_detail" verbose_name = verbose_name_plural = '单个服务回滚记录' def get_service_history(self): """ 组织服务操作记录参数 :return: dict """ return { "username": self.history.operator.username, "description": f"服务自版本{self.upgrade.target_app.app_version}" f"回滚至版本{self.upgrade.current_app.app_version}", "result": "success" if self.rollback_state == 2 else "failed", "created": self.created } ================================================ FILE: omp_server/db_models/models/upload.py ================================================ import os import random import string from datetime import datetime from django.conf import settings from django.core.files.storage import FileSystemStorage from django.db import models from utils.plugin.public_utils import file_md5, format_location_size from .user import UserProfile from db_models.mixins import TimeStampMixin class UploadFileHistory(TimeStampMixin): STORAGE_KLASS = {"location"} module = models.CharField("需要上传文件的model", max_length=32, default="") module_id = models.IntegerField("需要上传文件的model id", default=0) union_id = models.CharField("文件md5值", max_length=64, default="") user = models.ForeignKey( UserProfile, null=True, on_delete=models.SET_NULL, verbose_name="用户") # 目前只支持location storage_klass = models.CharField( "存储方式", max_length=64, default="location") # location时相对于omp目录 relative_path = models.TextField("文件存储相对路径", default="") file_name = models.CharField("文件名称", max_length=64, default="") # 单位:K,M,G file_size = models.CharField("文件大小", max_length=16, default="0K") # location时除ip外url file_url = models.TextField("文件访问路径", default="") deleted = models.BooleanField("删除", default=False) class Meta: db_table = "omp_upload_file" verbose_name = verbose_name_plural = "上传文件记录" @classmethod def format_file_name(cls, file_name): _time = datetime.now().strftime('%Y%m%d%H%M%S') random_sub = ''.join( random.sample(string.digits+string.ascii_lowercase, 4)) if len(file_name) > 10: file_name = file_name[-10:] return f"{_time}-{random_sub}-{file_name}" @classmethod def location(cls, file, module_obj=None, user=None, **kwargs): cls_kwargs = {"relative_path": 'tmp/', "user": user} if module_obj: cls_kwargs = { "module": module_obj.__class__.__name__, "module_id": module_obj.id, } relative_path = module_obj.valid_upload_file(file, **kwargs) cls_kwargs["relative_path"] = relative_path else: relative_path = 'tmp/' save_path = os.path.join(settings.PROJECT_DIR, relative_path) storage = FileSystemStorage(save_path) _time = datetime.now().strftime('%Y%m%d%H%M%S') file_name = cls.format_file_name(file.name) cls_kwargs["file_name"] = file_name if module_obj: cls_kwargs["file_url"] = module_obj.upload_file_url(file_name) else: cls_kwargs["file_url"] = f"download/{file_name}" storage.save(storage.generate_filename(file_name), file) # FileSystemStorage save 同时计算md5? md5 = file_md5(os.path.join(save_path, file_name)) if md5: exists_obj = cls.objects.filter(union_id=md5).last() if exists_obj: old_file = os.path.join( settings.PROJECT_DIR, exists_obj.relative_path, exists_obj.file_name ) if os.path.exists(old_file): os.remove(os.path.join(save_path, file_name)) else: exists_obj.file_name = file_name exists_obj.relative_path = relative_path exists_obj.save() return exists_obj cls_kwargs["file_size"] = format_location_size(file.size) return cls.objects.create(union_id=md5, **cls_kwargs) ================================================ FILE: omp_server/db_models/models/user.py ================================================ from django.contrib.auth.models import AbstractUser from django.db import models class UserProfile(AbstractUser): """ 自定义用户表 """ role = models.CharField("用户角色", max_length=128, null=True, blank=True, help_text="用户角色") class Meta: """ 元数据 """ db_table = "omp_user_profile" verbose_name = verbose_name_plural = "用户" def __str__(self): """ 显示用户 """ return f"用户: {self.username}" class OperateLog(models.Model): """ 用户操作记录表 """ objects = None username = models.CharField( "操作用户", max_length=128, help_text="操作用户") request_method = models.CharField( "请求方法", max_length=32, help_text="请求方法") request_ip = models.GenericIPAddressField( "请求源IP", blank=True, null=True, help_text="请求源IP") request_url = models.CharField( "用户目标URL", max_length=256, help_text="用户目标URL") description = models.CharField( "用户行为描述", max_length=256, help_text="用户行为描述") response_code = models.IntegerField( "响应状态码", default=0, help_text="响应状态码") request_result = models.TextField( "请求结果", default="success", help_text="请求结果") create_time = models.DateTimeField( "操作发生时间", auto_now_add=True, help_text="操作发生时间") class Meta: """ 元数据 """ db_table = "omp_user_operate_log" verbose_name = verbose_name_plural = "用户操作记录" class UserLoginLog(models.Model): """用户登陆记录""" objects = None username = models.CharField(max_length=128, verbose_name="Username") login_time = models.DateTimeField( blank=True, null=True, verbose_name="Login time") ip = models.CharField(max_length=32, null=True, blank=True, verbose_name="Login ip") role = models.CharField( max_length=128, verbose_name="role ", null=True, blank=True) request_result = models.CharField( "请求结果", max_length=512, null=True, blank=True, help_text="请求结果") class Meta: db_table = "omp_login_log" verbose_name = verbose_name_plural = "用户登陆记录" ================================================ FILE: omp_server/db_models/receivers/__init__.py ================================================ from .execution_record import install_execution_record,\ upgrade_execution_record, rollback_execution_record from .service import update_execution_record __all__ = [ install_execution_record, upgrade_execution_record, rollback_execution_record, update_execution_record ] ================================================ FILE: omp_server/db_models/receivers/execution_record.py ================================================ from django.dispatch import receiver from django.db.models.signals import post_save from db_models.models import ExecutionRecord, MainInstallHistory, \ UpgradeHistory, RollbackHistory from db_models.models.execution import StateChoices def create_execution_record(instance, created=False, *args, **kwargs): module = instance.__class__.__name__ obj, _ = ExecutionRecord.objects.get_or_create( module=module, module_id=instance.module_id ) old_state = obj.state state = f"{module.upper()}_{instance.execution_record_state}" obj.state = getattr(StateChoices, state) if isinstance(instance.operator, str): obj.operator = instance.operator else: obj.operator = instance.operator.username obj.created = instance.created if ("SUCCESS" in obj.state or "FAIL" in obj.state) and \ obj.state != old_state: obj.end_time = instance.modified obj.count = instance.operate_count() obj.save() @receiver(post_save, sender=MainInstallHistory) def install_execution_record(sender, instance, *args, **kwargs): create_execution_record(instance, *args, **kwargs) @receiver(post_save, sender=UpgradeHistory) def upgrade_execution_record(sender, instance, created, *args, **kwargs): create_execution_record(instance, created, *args, **kwargs) @receiver(post_save, sender=RollbackHistory) def rollback_execution_record(sender, instance, created, *args, **kwargs): create_execution_record(instance, created, *args, **kwargs) ================================================ FILE: omp_server/db_models/receivers/service.py ================================================ from django.db.models.signals import pre_delete, post_save from django.dispatch import receiver from db_models.mixins import UpgradeStateChoices, RollbackStateChoices from db_models.models import Service, MainInstallHistory, \ ExecutionRecord, UpgradeHistory, RollbackHistory, UpgradeDetail, \ RollbackDetail, DetailInstallHistory, SelfHealingSetting from utils.plugin.crontab_utils import change_task def update_upgrade_history(history, union_server): UpgradeDetail.objects.filter( union_server=union_server).delete() details = history.upgradedetail_set.exclude( upgrade_state=UpgradeStateChoices.UPGRADE_SUCCESS ).exclude(union_server=union_server) if not details.exists(): history.upgrade_state = UpgradeStateChoices.UPGRADE_SUCCESS history.save() def update_rollback_history(history, union_server): RollbackDetail.objects.filter( upgrade__union_server=union_server).delete() details = history.rollbackdetail_set.exclude( rollback_state=RollbackStateChoices.ROLLBACK_SUCCESS ).exclude(upgrade__union_server=union_server) if not details.exists(): history.rollback_state = RollbackStateChoices.ROLLBACK_SUCCESS history.save() @receiver(pre_delete, sender=Service) def update_execution_record(sender, instance, *args, **kwargs): # models.SET_NULL必须改! filter_keys = [ (MainInstallHistory, "detailinstallhistory__service"), (RollbackHistory, "rollbackdetail__upgrade__service"), (UpgradeHistory, "upgradedetail__service"), ] if instance.service.app_name == "hadoop" and instance.service_split == 2: Service.split_objects.filter( ip=instance.ip, service__app_name="hadoop" ).delete() union_server = f"{instance.ip}-{instance.service.app_name}" for model_cls, filter_key in filter_keys: history = model_cls.objects.filter(**{filter_key: instance}).first() if not history: continue execution_record = ExecutionRecord.objects.filter( module=history.__class__.__name__, module_id=history.module_id ).first() if not execution_record: continue execution_record.count = history.operate_count([instance.id]) execution_record.save() if model_cls.__name__ == "RollbackHistory" and \ history.rollback_state != RollbackStateChoices.ROLLBACK_SUCCESS: update_rollback_history(history, union_server) if model_cls.__name__ == "UpgradeHistory" and \ history.upgrade_state != UpgradeStateChoices.UPGRADE_SUCCESS: update_upgrade_history(history, union_server) # 删除安装记录, 修复卸载产品再重试安装 DetailInstallHistory.objects.filter(service=instance).delete() @receiver(post_save, sender=SelfHealingSetting) def update_self_health(sender, instance, *args, **kwargs): data = {'is_on': instance.used, 'task_func': 'services.self_healing.self_healing', 'task_name': f'self_health_cron_task_{instance.id}', 'crontab_detail': dict(day_of_month='*', day_of_week='*', hour="*", minute=f"*/{instance.fresh_rate}", month_of_year='*')} change_task(instance.id, data=data) @receiver(pre_delete, sender=SelfHealingSetting) def delete_self_health(sender, instance, *args, **kwargs): data = { "is_on": False, 'task_func': 'services.self_healing.self_healing', 'task_name': f'self_health_cron_task_{instance.id}', } change_task(instance.id, data=data) ================================================ FILE: omp_server/db_models/signals/__init__.py ================================================ __all__ = [ ] ================================================ FILE: omp_server/dev_code.md ================================================ # 项目及研发规范 ## 前后端接口规范 ### 后端正确响应返回格式 ```json { "code": 0, "message": "success", "data": null } ``` ### 后端错误响应返回格式 ```json { "code": 1, "message": "错误描述信息", "data": null } ``` # API文档 ================================================ FILE: omp_server/dev_requirement.txt ================================================ amqp==5.0.6 asgiref==3.4.1 aspy.yaml==1.3.0 attrs==21.2.0 backports.entry-points-selectable==1.1.0 bcrypt==3.2.0 billiard==3.6.4.0 boto3==1.16.42 botocore==1.19.42 cached-property==1.5.2 celery==5.1.2 certifi==2021.5.30 cffi==1.14.4 cfgv==3.3.1 chardet==3.0.4 charset-normalizer==2.0.5 click==7.1.2 click-didyoumean==0.0.3 click-plugins==1.1.1 click-repl==0.2.0 concurrent-log-handler==0.9.19 coreapi==2.3.3 coreschema==0.0.4 cryptography==3.3.1 distlib==0.3.2 distro==1.5.0 Django==3.1.4 django-celery-beat==2.2.1 django-celery-results==2.2.0 django-filter==2.4.0 django-ipware==4.0.0 django-timezone-field==4.2.1 djangorestframework==3.12.4 djangorestframework-jwt==1.11.0 emoji==1.5.0 et-xmlfile==1.1.0 filelock==3.0.12 identify==2.2.14 idna==3.2 iniconfig==1.1.1 ipaddress==1.0.23 itypes==1.2.0 jdcal==1.4.1 Jinja2==3.0.1 jmespath==0.10.0 kombu==5.1.0 MarkupSafe==2.0.1 msgpack==1.0.2 nodeenv==1.6.0 packaging==21.0 paramiko==2.7.2 Pillow==9.1.0 platformdirs==2.3.0 pluggy==1.0.0 portalocker==2.3.0 pre-commit==2.15.0 prompt-toolkit==3.0.20 psutil==5.8.0 py==1.10.0 pycparser==2.20 pycryptodome==3.10.1 pycryptodomex==3.9.9 PyJWT==1.7.1 PyMySQL==1.0.2 PyNaCl==1.4.0 pyparsing==2.4.7 pysqlite3==0.4.5 pytest==6.2.5 python-crontab==2.5.1 python-dateutil==2.8.2 pytz==2021.1 PyYAML==5.4.1 pyzmq==20.0.0 redis==3.5.3 requests==2.26.0 ruamel.yaml==0.17.10 ruamel.yaml.clib==0.2.6 s3transfer==0.3.3 salt==3002.2 scp==0.13.3 six==1.16.0 sqlparse==0.4.1 toml==0.10.2 tzlocal==2.1 uritemplate==3.0.1 urllib3==1.26.6 uWSGI==2.0.19.1 vine==5.0.0 virtualenv==20.7.2 wcwidth==0.2.5 Werkzeug==1.0.1 xlrd==1.2.0 ================================================ FILE: omp_server/hosts/__init__.py ================================================ ================================================ FILE: omp_server/hosts/admin.py ================================================ # from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/hosts/apps.py ================================================ from django.apps import AppConfig class HostsConfig(AppConfig): name = 'hosts' ================================================ FILE: omp_server/hosts/hosts_filters.py ================================================ """ 主机相关过滤器 """ import django_filters from django_filters.rest_framework import FilterSet from db_models.models import (Host, HostOperateLog) class HostFilter(FilterSet): """ 主机过滤类 """ ip = django_filters.CharFilter( help_text="IP,模糊匹配", field_name="ip", lookup_expr="icontains") class Meta: model = Host fields = ("ip",) class HostOperateFilter(FilterSet): """ 主机日志过滤类 """ host_id = django_filters.CharFilter( help_text="主机 ID", field_name="host_id", lookup_expr="exact") class Meta: model = HostOperateLog fields = ("host_id",) ================================================ FILE: omp_server/hosts/hosts_serializers.py ================================================ """ 主机序列化器 """ import logging import socket import struct from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.serializers import ( ModelSerializer, Serializer ) from db_models.models import ( Host, Env, HostOperateLog, Service ) from hosts.tasks import ( host_agent_restart, init_host, insert_host_celery_task, deploy_agent, reinstall_monitor_celery_task, delete_hosts ) from utils.plugin.ssh import SSH from utils.plugin.crypto import AESCryptor from utils.common.validators import ( ReValidator, NoEmojiValidator, NoChineseValidator ) from utils.common.exceptions import OperateError from utils.common.serializers import HostIdsSerializer from utils.parse_config import THREAD_POOL_MAX_WORKERS from promemonitor.alertmanager import Alertmanager logger = logging.getLogger("server") class HostUninstallSerializer(ModelSerializer): class Meta: """ 元数据 """ model = Host fields = ('id',) def validate(self, attrs): """ 校验主机是否存在服务 """ request_data = self.context.get('request').data host_list = request_data.get("host_ids", []) host_ls = list(Host.objects.filter( id__in=host_list).values_list('ip', flat=True)) service_obj = Service.objects.filter(ip__in=host_ls).exclude( service__is_base_env=True ) if service_obj.count() != 0: ip_str = ",".join(set(service_obj.values_list('ip', flat=True))) raise ValidationError(f"IP存在相关的服务:{ip_str}") attrs['host_ls'] = host_ls return attrs def create(self, validated_data): """ 删除主机 """ host_ls = validated_data.get("host_ls") agent_status = {"host_agent": Host.AGENT_DEPLOY_DELETE, "monitor_agent": Host.AGENT_DEPLOY_DELETE} Host.objects.filter(ip__in=host_ls).update(**agent_status) delete_hosts.delay(host_ls) return "任务下发成功" class HostSerializer(ModelSerializer): """ 主机序列化类 """ instance_name = serializers.CharField( help_text="实例名", required=True, max_length=16, error_messages={ "required": "必须包含[instance_name]字段", "max_length": "实例名长度需小于{max_length}"}, validators=[ NoEmojiValidator(), NoChineseValidator(), ReValidator(regex=r"^[-a-zA-Z0-9].*$"), ]) ip = serializers.IPAddressField( help_text="IP地址", required=True, error_messages={ "invalid": "IP格式不合法", "required": "必须包含[ip]字段", }) port = serializers.IntegerField( help_text="端口", required=True, min_value=1, max_value=65535, error_messages={ "invalid": "端口格式不合法", "required": "必须包含[port]字段", "max_value": "端口超出指定范围", "min_value": "端口超出指定范围", }) username = serializers.CharField( help_text="用户名", required=True, max_length=16, error_messages={ "required": "必须包含[username]字段", "max_length": "用户名长度需小于{max_length}"}, validators=[ ReValidator(regex=r"^[_a-zA-Z0-9][-_a-zA-Z0-9]+$"), ]) password = serializers.CharField( help_text="密码", required=True, min_length=8, max_length=64, error_messages={ "required": "必须包含[password]字段", "min_length": "密码长度需大于{min_length}", "max_length": "密码长度需小于{max_length}"}, validators=[ NoEmojiValidator(), NoChineseValidator(), ]) data_folder = serializers.CharField( help_text="数据分区", required=True, max_length=255, error_messages={"required": "必须包含[data_folder]字段"}, validators=[ ReValidator(regex=r"^/[-_/a-zA-Z0-9]+$"), ]) operate_system = serializers.CharField( help_text="操作系统", required=True, max_length=128, error_messages={"required": "必须包含[operate_system]字段"}) env = serializers.PrimaryKeyRelatedField( help_text="环境", required=False, queryset=Env.objects.all(), error_messages={"does_not_exist": "未找到对应环境"}) run_user = serializers.CharField( help_text="运行用户", required=False, default="", max_length=16, write_only=True, error_messages={ "max_length": "运行用户长度需小于{max_length}"}, validators=[ ReValidator(regex=r"^[_a-zA-Z0-9][-_a-zA-Z0-9]+$"), ]) use_ntpd = serializers.BooleanField( help_text="是否开启时钟同步", required=True, error_messages={"required": "必须包含[use_ntpd]字段"} ) ntpd_server = serializers.IPAddressField( help_text="时间同步服务器IP地址", required=False, error_messages={ "invalid": "ntpd_server格式不合法" } ) class Meta: """ 元数据 """ model = Host exclude = ("is_deleted", "agent_dir",) read_only_fields = ( "service_num", "alert_num", "host_name", "operate_system", "memory", "cpu", "disk", "is_maintenance", "host_agent", "monitor_agent", "host_agent_error", "monitor_agent_error", "init_status" ) # def get_service_num(self, obj): # """ # 获取主机上的 # :param obj: # :return: # """ # return Service.objects.filter( # ip=obj.ip, service__is_base_env=False).count() def validate_instance_name(self, instance_name): """ 校验实例名是否重复 """ queryset = Host.objects.all() if self.instance is not None: queryset = queryset.exclude(id=self.instance.id) if queryset.filter(instance_name=instance_name).exists(): raise ValidationError("实例名已经存在") return instance_name def validate_ip(self, ip): """ 校验IP是否重复 """ if self.instance is not None: if ip != self.instance.ip: raise ValidationError("IP不可修改") return ip if Host.objects.filter(ip=ip).exists(): raise ValidationError("IP已经存在") return ip def validate_data_folder(self, data_folder): """ 校验数据分区是否合理 """ dir_ls = data_folder.split("/") for dir_name in dir_ls: if dir_name != "" and dir_name.startswith("-"): raise ValidationError("数据分区目录不能以'-'开头") return data_folder def validate_operate_system(self, operate_system): """ 校验操作系统是否合法 """ operate_ls = ("CentOS", "RedHat") if operate_system not in operate_ls: raise ValidationError(f"操作系统支持{'/'.join(operate_ls)}") return operate_system def validate(self, attrs): """ 主机信息验证 """ ip = attrs.get("ip") port = attrs.get("port") username = attrs.get("username") password = attrs.get("password") data_folder = attrs.get("data_folder") run_user = attrs.get("run_user") use_ntpd = attrs.get("use_ntpd") # 默认主机初始化标记为 False attrs["init_host"] = False # 如果提供 run_user,需确保用户为 root if run_user and username != "root": raise ValidationError({"username": "运行用户仅在用户名为root时可用"}) # 校验主机 SSH 连通性 ssh = SSH(ip, port, username, password) is_connect, _ = ssh.check() if not is_connect: logger.info(f"host ssh connection failed: ip-{ip},port-{port}," f"username-{username},password-{password}") raise ValidationError({"ip": "SSH登录失败"}) # 如果数据分区不存在,则创建数据分区 success, msg = ssh.cmd( f"test -d {data_folder} || mkdir -p {data_folder}") if not success or msg.strip(): logger.info(f"host create data folder failed: ip-{ip},port-{port}," f"username-{username},password-{password}," f"data_folder-{data_folder}") raise ValidationError({"data_folder": "创建数据分区操作失败"}) # 当用户为 root 或具有 sudo 权限时,自动进行初始化 is_sudo, _ = ssh.is_sudo() if is_sudo or username == "root": attrs["init_host"] = True # 如果未传递 env,则指定默认环境 if not attrs.get("env") and not self.instance: attrs["env"] = Env.objects.filter(id=1).first() # 主机密码加密处理 if attrs.get("password"): attrs["password"] = AESCryptor().encode(attrs.get("password")) # 启用ntpd,验证ntpd服务器是否可用 if use_ntpd: ntpd_server = attrs.get("ntpd_server") # udp 检测ip:port是否可用 REF_TIME_1970 = 2208988800 client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) data = b'\x1b' + 47 * b'\0' ip_port = (ntpd_server, 123) client.sendto(data, ip_port) client.settimeout(20) try: data, address = client.recvfrom(1024) except socket.timeout as e: logger.info(f"ntpd服务端不可用:{e}") data = bytes('', encoding='utf-8') t = 0 if data: t = struct.unpack('!12I', data)[10] t -= REF_TIME_1970 if t == 0: raise ValidationError(f"{ntpd_server}上的ntpd服务端不可用") return attrs def create(self, validated_data): """ 创建主机 """ ip = validated_data.get("ip") init_flag = validated_data.pop("init_host", False) # 如果 run_user 存在,则删除 if "run_user" in validated_data: validated_data.pop("run_user") # 指定 Agent 安装目录为 data_folder validated_data["agent_dir"] = validated_data.get("data_folder") instance = super(HostSerializer, self).create(validated_data) logger.info(f"host[{ip}] - create success") # 写入操作记录 HostOperateLog.objects.create( username=self.context["request"].user.username, description="创建主机", host=instance) # 下发异步任务: 初始化主机、安装 Agent logger.info(f"host[{ip}] - ADD celery task") insert_host_celery_task.delay( instance.id, init=init_flag) # deploy_agent.delay(instance.id) return instance def update(self, instance, validated_data): """ 更新主机 """ validated_data.pop("init_host") # 如果 run_user 存在,则删除 if "run_user" in validated_data: validated_data.pop("run_user") log_ls = [] username = self.context["request"].user.username # 获取所有发生修改字段 for key, new_value in validated_data.items(): old_value = getattr(instance, key) if old_value != new_value: description = f"修改[{getattr(Host, key).field.help_text}]" if key != "password": description += f": 由[{getattr(instance, key)}]修改为[{new_value}]" log_ls.append(HostOperateLog( username=username, description=description, host=instance)) # 写入主机操作记录表中 HostOperateLog.objects.bulk_create(log_ls) return super(HostSerializer, self).update(instance, validated_data) class HostOperateLogSerializer(ModelSerializer): """ 主机操作记录序列化器类 """ class Meta: """ 元数据 """ model = HostOperateLog fields = '__all__' class HostDetailSerializer(ModelSerializer): """ 主机详细信息序列化类 """ history = HostOperateLogSerializer( source="hostoperatelog_set.all", many=True) deployment_information = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = Host exclude = ("is_deleted", "agent_dir", "password", "host_name", "env", "host_agent_error", "monitor_agent_error") def get_deployment_information(self, obj): result_ls = [] base_env_queryset = Service.objects.filter( ip=obj.ip, service__is_base_env=True) host_env_queryset = Host.objects.filter(ip=obj.ip).exclude( ntpdate_install_status=Host.NTPDATE_NOT_INSTALL) if base_env_queryset.exists(): result_ls = list(base_env_queryset.values( "service__app_name", "service__app_version", "service__app_logo")) if host_env_queryset.exists(): result_ls.append( {"service__app_name": "ntpdate", "service__app_version": "4.2.8", "service__app_logo": "" } ) return result_ls class HostFieldCheckSerializer(ModelSerializer): """ 主机字段重复性校验序列化器 """ id = serializers.IntegerField( help_text="主机ID,更新时需要此字段", required=False) instance_name = serializers.CharField( help_text="实例名", max_length=16, required=False, validators=[ NoEmojiValidator(), NoChineseValidator(), ReValidator(regex=r"^[-a-zA-Z0-9].*$"), ]) ip = serializers.IPAddressField( help_text="IP地址", required=False) class Meta: """ 元数据 """ model = Host fields = ("id", "instance_name", "ip",) def validate(self, attrs): """ 校验 instance_name / ip 是否重复 """ host_id = attrs.get("id") instance_name = attrs.get("instance_name") ip = attrs.get("ip") queryset = Host.objects.all() if host_id is not None: queryset = queryset.exclude(id=host_id) if instance_name and \ queryset.filter(instance_name=instance_name).exists(): raise ValidationError({"instance_name": "实例名已经存在"}) if ip and queryset.filter(ip=ip).exists(): raise ValidationError({"ip": "IP已经存在"}) return attrs class HostMaintenanceSerializer(HostIdsSerializer): """ 主机维护模式序列化类 """ is_maintenance = serializers.BooleanField( help_text="开启/关闭维护模式", required=True, error_messages={"required": "必须包含[is_maintenance]字段"}) def write_host_log(self, host_queryset, status, result): """ 写入主机日志 """ log_ls = [] for host in host_queryset: log_ls.append(HostOperateLog( username=self.context["request"].user.username, description=f"{status}[维护模式]", result=result, host=host)) HostOperateLog.objects.bulk_create(log_ls) def validate(self, attrs): """ 校验列表中主机 '维护模式' 字段值是否正确 """ queryset = Host.objects.filter( id__in=attrs.get("host_ids"), is_maintenance=attrs.get("is_maintenance")) if queryset.exists(): status = "开启" if attrs.get("is_maintenance") else "关闭" raise ValidationError({ "host_ids": f"主机列表中存在已 '{status}' 维护模式的主机" }) return attrs def create(self, validated_data): """ 进入 / 退出维护模式 """ host_ids = validated_data.get("host_ids") is_maintenance = validated_data.get("is_maintenance") status = "开启" if is_maintenance else "关闭" en_status = "open" if is_maintenance else "close" host_queryset = Host.objects.filter(id__in=host_ids) host_ls = list(host_queryset.values("ip")) # 根据 is_maintenance 判断主机进入 / 退出维护模式 alert_manager = Alertmanager() if is_maintenance: res_ls = alert_manager.set_maintain_by_host_list(host_ls) else: res_ls = alert_manager.revoke_maintain_by_host_list(host_ls) # 操作失败 if not res_ls: logger.error(f"host {en_status} maintain failed: {host_ids}") # 操作失败记录写入 self.write_host_log(host_queryset, status, "failed") raise OperateError(f"主机'{status}'维护模式失败") # 操作成功 host_queryset.update(is_maintenance=is_maintenance) logger.info(f"host {en_status} maintain success: {host_ids}") # 操作成功记录写入 self.write_host_log(host_queryset, status, "success") return validated_data class HostAgentRestartSerializer(HostIdsSerializer): """ 主机Agent重启序列化类 """ def create(self, validated_data): """ 主机Agent重启 """ host_ids = validated_data.get("host_ids", []) filter_host_ids = list( Host.objects.filter( id__in=host_ids, host_agent__in=[ str(Host.AGENT_RUNNING), str(Host.AGENT_RESTART), str(Host.AGENT_START_ERROR) ] ).values_list("id", flat=True) ) for item in filter_host_ids: host_agent_restart.delay(item) # 下发任务后批量更新重启主机状态 Host.objects.filter( id__in=filter_host_ids ).update(host_agent=Host.AGENT_RESTART) return validated_data class HostBatchValidateSerializer(Serializer): """ 主机数据批量验证序列化类 """ host_list = serializers.ListField( child=serializers.DictField(), help_text="主机数据列表", required=True, allow_empty=False, error_messages={"required": "必须包含[host_list]字段"} ) def host_info_validate(self, host_data): """ 单个主机信息验证 """ host_serializer = HostSerializer(data=host_data) if host_serializer.is_valid(): host_data["init_host"] = \ host_serializer.validated_data.get("init_host") return "correct", host_data err_ls = [] ip_err = "Enter a valid IPv4 or IPv6 address." for k, v in host_serializer.errors.items(): if ip_err in v: v = [f"{k} 格式不合法"] err_ls.extend(v) host_data["validate_error"] = "; ".join(err_ls) return "error", host_data def validate(self, attrs): """ 校验主机数据列表 """ host_list = attrs.get("host_list")[:] result_dict = { "correct": [], "error": [] } logger.info(f"host batch validate start: {host_list}") # 校验主机列表中是否存在相同实例名或IP的数据 no_repeat_host = [] instance_name_list = list( map(lambda x: x.get("instance_name"), host_list)) ip_list = list(map(lambda x: x.get("ip"), host_list)) for index, host in enumerate(host_list): repeat_ls = [] if instance_name_list.count(host.get("instance_name")) > 1: repeat_ls.append("实例名") if ip_list.count(host.get("ip")) > 1: repeat_ls.append("IP") if repeat_ls: host_info = host_list[index] host_info["validate_error"] = f"{'、'.join(repeat_ls)}在表格中重复" result_dict["error"].append(host_info) continue no_repeat_host.append(host_list[index]) # 校验主机数据正确性 with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: future_list = [] for host_data in no_repeat_host: future_obj = executor.submit( self.host_info_validate, host_data) future_list.append(future_obj) for future in as_completed(future_list): flag, host_data = future.result() result_dict[flag].append(host_data) # 按照 row 行号对列表进行排序 for v in result_dict.values(): if len(v) > 0: v.sort(key=lambda x: x.get("row", 999)) attrs["result_dict"] = result_dict logger.info("host batch validate end") return attrs class HostBatchImportSerializer(Serializer): """ 主机数据批量创建序列化类 """ host_list = serializers.ListField( child=serializers.DictField(), help_text="主机数据列表", required=True, allow_empty=False, error_messages={"required": "必须包含[host_list]字段"} ) class HostInitSerializer(HostIdsSerializer): """ 主机初始化序列化类 """ def create(self, validated_data): """ 主机初始化 """ host_ids = validated_data.get("host_ids", []) for host_id in host_ids: init_host.delay(host_id) # 下发任务后批量更新主机初始化状态 Host.objects.filter( id__in=host_ids ).update(init_status=Host.INIT_EXECUTING) return validated_data class HostsAgentStatusSerializer(Serializer): """ 主机 agent 状态序列化类 """ ip_list = serializers.ListField( child=serializers.CharField(), help_text="主机ip列表", required=True, allow_empty=False, error_messages={"required": "必须包含[ip_list]字段"} ) class HostReinstallSerializer(HostIdsSerializer): """ 主机重新安装序列化类 """ def create(self, validated_data): """ 主机重装 """ host_ids = validated_data.get("host_ids", []) # 不重装监控agent for host_id in host_ids: deploy_agent.delay(host_id, need_monitor=False) Host.objects.filter( id__in=host_ids ).update(host_agent=Host.AGENT_DEPLOY_ING) return validated_data class MonitorReinstallSerializer(HostIdsSerializer): """ 监控重新安装序列化类 """ def create(self, validated_data): """ 监控重装 """ host_ids = validated_data.get("host_ids", []) user_name = self.context["request"].user.username for host_id in host_ids: reinstall_monitor_celery_task.delay(host_id, user_name) Host.objects.filter( id__in=host_ids ).update(monitor_agent=Host.AGENT_DEPLOY_ING) return validated_data ================================================ FILE: omp_server/hosts/tasks.py ================================================ # -*- coding: utf-8 -*- # Project: tasks # Author: jon.liu@yunzhihui.com # Create time: 2021-09-12 11:54 # IDE: PyCharm # Version: 1.0 # Introduction: """ 主机相关异步任务 """ import os import time import logging import traceback import subprocess import requests import json from django.conf import settings from celery import shared_task from celery.utils.log import get_task_logger from promemonitor.alertmanager import Alertmanager from promemonitor.prometheus_utils import PrometheusUtils from db_models.models import ( Host, Service, HostOperateLog, Alert ) from utils.plugin.ssh import SSH from utils.plugin.monitor_agent import MonitorAgentManager from utils.plugin.crypto import AESCryptor from utils.plugin.agent_util import Agent from app_store.tasks import add_prometheus from utils.parse_config import HOSTNAME_PREFIX from utils.plugin.install_ntpdate import InstallNtpdate from omp_server.settings import PROJECT_DIR from concurrent.futures import ThreadPoolExecutor # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") def deploy_monitor_agent(host_obj, salt_flag=True): """ 部署监控Agent :param host_obj: 主机对象 :param salt_flag: 部署主机Agent成功或失败标志 :return: """ logger.info(f"Deploy monitor agent for {host_obj.ip}") if not salt_flag: Host.objects.filter(ip=host_obj.ip).update( monitor_agent=4, monitor_agent_error="主机salt-agent部署失败!" ) logger.error( "Deploy monitor agent failed because salt agent deploy failed") return monitor_manager = MonitorAgentManager(host_objs=[host_obj]) install_flag, install_msg = monitor_manager.install() logger.info( f"Deploy monitor agent, " f"install_flag: {install_flag}; install_msg: {install_msg}") if not install_flag: Host.objects.filter(ip=host_obj.ip).update( monitor_agent=4, monitor_agent_error=install_msg if len( install_msg) < 200 else install_flag[:200] ) else: Host.objects.filter(ip=host_obj.ip).update(monitor_agent=0) def real_deploy_agent(host_obj, need_monitor=True): """ 部署主机Agent :param host_obj: 主机对象 :type host_obj Host :param need_monitor: 是否部署monitor :type need_monitor bool :return: """ logger.info( f"Deploy Agent for {host_obj.ip}, Params: " f"username: {host_obj.username}; " f"port: {host_obj.port}; " f"install_dir: {host_obj.agent_dir}!") _obj = Agent( host=host_obj.ip, port=host_obj.port, username=host_obj.username, password=AESCryptor().decode(host_obj.password), install_dir=host_obj.agent_dir ) flag, message = _obj.agent_deploy() logger.info( f"Deploy Agent for {host_obj.ip}, " f"Res Flag: {flag}; Res Message: {message}") # 更新主机Agent状态,0 正常;4 部署失败 # 使用filter查询然后使用update方法进行处理,防止多任务环境 if flag: Host.objects.filter(ip=host_obj.ip).update(host_agent=0) else: Host.objects.filter(ip=host_obj.ip).update( host_agent=4, host_agent_error=str(message)[:200] if len( str(message)) > 200 else str(message) ) # 部署监控agent if need_monitor: deploy_monitor_agent(host_obj=host_obj, salt_flag=flag) @shared_task def deploy_agent(host_id, need_monitor=True): """ 部署主机Agent :param host_id: :param need_monitor: 是否部署monitor :type need_monitor bool :return: """ try: # edit by vum: # obj.save 在异步任务并发读写下存在数值覆盖问题 host_query = Host.objects.filter(id=host_id) host_query.update(host_agent=3) real_deploy_agent( host_obj=host_query.first(), need_monitor=need_monitor ) except Exception as e: logger.error( f"Deploy Host Agent For {host_id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( host_agent=4, host_agent_error=str(e)) def real_host_agent_restart(host_obj): """ 重启主机Agent :param host_obj: 主机对象 :type host_obj Host :return: """ logger.info( f"Restart Agent for {host_obj.ip}, Params: " f"username: {host_obj.username}; " f"port: {host_obj.port}; " f"install_dir: {host_obj.agent_dir}!") _obj = SSH( hostname=host_obj.ip, port=host_obj.port, username=host_obj.username, password=AESCryptor().decode(host_obj.password), ) _script_path = os.path.join( host_obj.agent_dir, "omp_salt_agent/bin/omp_salt_agent") flag, message = _obj.cmd(f"bash {_script_path} restart") logger.info( f"Restart host agent for {host_obj.ip}: " f"get flag: {flag}; get res: {message}") # 使用filter查询然后使用update方法进行处理,防止多任务环境 if flag: # host_obj.host_agent = 0 Host.objects.filter(ip=host_obj.ip).update(host_agent=0) else: # host_obj.host_agent = 2 # host_obj.host_agent_error = \ # str(message)[:200] if len(str(message)) > 200 else str(message) Host.objects.filter(ip=host_obj.ip).update( host_agent=2, host_agent_error=str(message)[:200] if len( str(message)) > 200 else str(message) ) @shared_task def host_agent_restart(host_id): """ 主机Agent的重启操作 :param host_id: 主机的id :return: """ try: host_obj = Host.objects.get(id=host_id) real_host_agent_restart(host_obj=host_obj) except Exception as e: logger.error( f"Restart Host Agent For {host_id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( host_agent=2, host_agent_error=str(e)) def real_init_host(host_obj): """ 初始化主机 :param host_obj: 主机对象 :type host_obj Host :return: """ logger.info(f"init host Begin [{host_obj.id}]") _ssh = SSH( hostname=host_obj.ip, port=host_obj.port, username=host_obj.username, password=AESCryptor().decode(host_obj.password), ) # 验证用户权限 is_sudo, _ = _ssh.is_sudo() if not is_sudo: logger.error(f"init host [{host_obj.id}] failed: permission failed") raise Exception("permission failed") # 发送脚本 init_script_name = "init_host.py" init_script_path = os.path.join( settings.BASE_DIR.parent, "package_hub", "_modules", init_script_name) script_push_state, script_push_msg = _ssh.file_push( file=init_script_path, remote_path="/tmp", ) if not script_push_state: logger.error(f"init host [{host_obj.id}] failed: send script failed, " f"detail: {script_push_msg}") raise Exception("send script failed") modified_host_name = str(HOSTNAME_PREFIX) + "".join( item.zfill(3) for item in host_obj.ip.split(".")) # 执行初始化 is_success, script_msg = _ssh.cmd( f"python /tmp/{init_script_name} init_valid {modified_host_name} {host_obj.ip}") if not (is_success and "init success" in script_msg and "valid success" in script_msg): logger.error(f"init host [{host_obj.id}] failed: execute init failed, " f"detail: {script_push_msg}") raise Exception("execute failed") Host.objects.filter( id=host_obj.id ).update(init_status=Host.INIT_SUCCESS) logger.info("init host Success") @shared_task def init_host(host_id): """ 初始化主机 """ try: # edit by vum: # obj.save 在异步任务并发读写下存在数值覆盖问题 host_query = Host.objects.filter(id=host_id) host_query.update(init_status=Host.INIT_EXECUTING) real_init_host(host_obj=host_query.first()) except Exception as e: print(e) logger.error( f"Init Host For {host_id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( init_status=Host.INIT_FAILED) @shared_task def insert_host_celery_task(host_id, init=False): """ 添加主机 celery 任务 """ # 执行主机初始化 if init: try: num = 0 host_obj = Host.objects.filter(id=host_id).first() while host_obj is None and num < 10: host_obj = Host.objects.filter(id=host_id).first() time.sleep(2) num += 1 if host_obj is None: raise Exception("Host Object not found") # edit by vum: # obj.save 在异步任务并发读写下存在数值覆盖问题 host_query = Host.objects.filter(id=host_id) host_query.update(init_status=Host.INIT_EXECUTING) real_init_host(host_obj=host_query.first()) except Exception as e: print(e) logger.error( f"Init Host For {host_id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( init_status=Host.INIT_FAILED) # 部署 agent try: num = 0 host_obj = Host.objects.filter(id=host_id).first() while host_obj is None and num < 10: host_obj = Host.objects.filter(id=host_id).first() time.sleep(2) num += 1 if host_obj is None: raise Exception("Host Object not found") host_query = Host.objects.filter(id=host_id) host_query.update(host_agent=Host.AGENT_DEPLOY_ING) real_deploy_agent(host_obj=host_query.first()) except Exception as e: logger.error( f"Deploy Host Agent For {host_id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( host_agent=Host.AGENT_DEPLOY_ERROR, host_agent_error=str(e)) # 部署ntpdate try: host_obj = Host.objects.filter(id=host_id).first() host_id = host_obj.id if host_obj.use_ntpd: InstallNtpdate(host_obj_list=[host_obj]).install() except Exception as e: logger.error( f"Deplot ntpdate for {id} Failed with error: {str(e)};\n" f"detail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( ntpdate_install_status=Host.NTPDATE_INSTALL_FAILED) def write_host_log(host_queryset, status, result, username): """ 写入主机日志 """ log_ls = [] for host in host_queryset: log_ls.append(HostOperateLog( username=username, description=f"{status}[维护模式]", result=result, host=host)) HostOperateLog.objects.bulk_create(log_ls) def maintenance(host_obj, entry, username): """ 进入 / 退出维护模式 """ # 根据 is_maintenance 判断主机进入 / 退出维护模式 status = "开启" if entry else "关闭" en_status = "open" if entry else "close" alert_manager = Alertmanager() host_ls = [{"ip": host_obj.ip}] if entry: res_ls = alert_manager.set_maintain_by_host_list(host_ls) else: res_ls = alert_manager.revoke_maintain_by_host_list(host_ls) # 操作失败 if not res_ls: logger.error(f"host {en_status} maintain failed: {host_obj.ip}") # 操作失败记录写入 write_host_log([host_obj], status, "failed", username) Host.objects.filter( id=host_obj.id ).update(is_maintenance=entry) logger.info(f"host {en_status} maintain success: {host_obj.ip}") # 操作成功记录写入 write_host_log([host_obj], status, "success", username) @shared_task def reinstall_monitor_celery_task(host_id, username): """ 重新安装主机监控 celery 任务 """ host_obj = Host.objects.filter(id=host_id).first() maintenance(host_obj, True, username) logger.info( f"Restart Agent for {host_obj.ip}, Params: " f"username: {host_obj.username}; " f"port: {host_obj.port}; " f"install_dir: {host_obj.agent_dir}!") _obj = SSH( hostname=host_obj.ip, port=host_obj.port, username=host_obj.username, password=AESCryptor().decode(host_obj.password), ) flag, message = _obj.cmd( "ps -ef | grep omp_monitor_agent | grep -v grep | awk -F ' ' '{print $2}' | xargs kill -9") logger.info( f"Stop monitor agent for {host_obj.ip}: " f"get flag: {flag}; get res: {message}") # 删除目录,防止agent_dir异常保护系统 if host_obj.agent_dir: monitor_dir = os.path.join(host_obj.agent_dir, "omp_monitor_agent") flag, message = _obj.cmd(f"/bin/rm -rf {monitor_dir}") logger.info( f"Stop monitor agent for {host_obj.ip}: " f"get flag: {flag}; get res: {message}") deploy_monitor_agent(host_obj=host_obj, salt_flag=flag) host_obj.refresh_from_db() if host_obj.monitor_agent == 4: maintenance(host_obj, False, username) return # 刷新prometheus服务列表监控配置,优化功能 service_obj_list = Service.objects.filter(ip=host_obj.ip) detail_obj_list = [] for service_obj in service_obj_list: detail_obj = service_obj.detailinstallhistory_set.first() if detail_obj: detail_obj_list.append(detail_obj) if len(detail_obj_list) != 0: add_prometheus(9999, detail_obj_list) maintenance(host_obj, False, username) class UninstallHosts(object): def __init__(self, all_host): self.is_success = True self.all_host = all_host @staticmethod def cmd(command): """执行本地shell命令""" p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) stdout, stderr = p.communicate() _out, _err, _code = stdout.decode( "utf8"), stderr.decode("utf8"), p.returncode return _out, _err, _code def delete_salt_key(self, key_list): """删除所有的salt-key""" python_path = os.path.join(PROJECT_DIR, 'component/env/bin/python3') salt_key_path = os.path.join(PROJECT_DIR, "component/env/bin/salt-key") salt_config_path = os.path.join(PROJECT_DIR, "config/salt") for item in key_list: _out, _err, _code = self.cmd( f"{python_path} {salt_key_path} -y -d '{item}' -c {salt_config_path}" ) if _code != 0: print(f"删除{item}获取到stdout: {_out}; stderr: {_err}") self.is_success = False logger.info(f"删除{item}获取到哦的stdout: {_out}; stderr: {_err}") @staticmethod def del_single_agent(obj): """ 删除单个节点的agent(salt and monitor) """ ip = obj.ip agent_dir = obj.agent_dir data_dir = obj.data_folder _ssh_obj = SSH( hostname=ip, port=obj.port, username=obj.username, password=AESCryptor().decode(obj.password) ) # 删除目录 if not data_dir: raise Exception(f"主机{ip}无数据目录") # TODO /app/bash_profile目前是指定目录 delete_cmd_str = f"rm -rf {data_dir}/omp_packages; " \ f"/bin/rm -rf {data_dir}/app/bash_profile; /bin/rm -rf /tmp/upgrade_openssl" cmd_res, msg = _ssh_obj.cmd( delete_cmd_str, timeout=120 ) logger.info(f"执行{ip} [delete] package and tmp 操作 {cmd_res}, 原因: {msg}") # 卸载salt agent salt_agent_dir = os.path.join(agent_dir, "omp_salt_agent") _delete_cron_cmd = "crontab -l|grep -v omp_salt_agent 2>/dev/null | crontab -;" _stop_agent = ( f"bash {salt_agent_dir}/bin/omp_salt_agent stop; /bin/rm -rf {salt_agent_dir}" ) final_cmd = f"{_delete_cron_cmd} {_stop_agent}" salt_res_flag, salt_res_msg = _ssh_obj.cmd(final_cmd, timeout=60) logger.info(f"卸载{ip}上的omp_salt_agent的命令为: {final_cmd}") logger.info( f"卸载{ip}上的omp_salt_agent的结果为: {salt_res_flag} {salt_res_msg}") # 卸载monitor agent monitor_agent_dir = os.path.join(agent_dir, "omp_monitor_agent") _delete_monitor_cron_cmd = "crontab -l|grep -v omp_monitor_agent " \ "2>/dev/null | crontab -;" _uninstall_monitor_agent_cmd = f"cd {monitor_agent_dir} &&" \ f" ./manage stop_all &&" \ f" bash monitor_agent.sh stop &&" \ f" cd {agent_dir} &&" \ f" /bin/rm -rf omp_monitor_agent" monitor_res_flag, monitor_res_msg = _ssh_obj.cmd( _uninstall_monitor_agent_cmd, timeout=120) res, msg = _ssh_obj.cmd( _delete_monitor_cron_cmd, timeout=120) cmd_ntpd_uninstall = "/bin/rm -rf {0}/app/ntpdate &&" \ "crontab -l| grep -v {0}/app/ntpdate 2>/dev/null" \ " | crontab -;".format(data_dir) if obj.username != "root": cmd_ntpd_uninstall = "sudo /bin/rm -rf {0}/app/ntpdate &&" \ "sudo crontab -l| grep -v {0}/app/ntpdate 2>/dev/null" \ " | sudo crontab -;".format(data_dir) ntpd_res, ntpd_msg = _ssh_obj.cmd( cmd_ntpd_uninstall, timeout=120) logger.info( f"卸载{ip}上的ntpd的结果为: {ntpd_res} {ntpd_msg}") logger.info( f"卸载{ip}上的omp_monitor_agent的命令为: {_uninstall_monitor_agent_cmd}") logger.info( f"卸载{ip}上的omp_monitor_agent的结果为: {monitor_res_flag} {monitor_res_msg}") if not all([cmd_res, salt_res_flag, monitor_res_flag]): return False, f"({ip}上卸载文件清除:{cmd_res}-{msg};\n salt:{salt_res_flag}-{salt_res_msg};\n monitor:{monitor_res_flag}-{monitor_res_msg};\n)" return True, "success" @staticmethod def execute_uninstall(host_obj_list, thread_name_prefix, function, max_num=8): """卸载执行函数""" thread_p = ThreadPoolExecutor( max_workers=max_num, thread_name_prefix=thread_name_prefix ) # future_list: [(ip, future),..] future_list = list() # result_list:[(ip, res_bool, res_msg), ...] result_list = list() for obj in host_obj_list: future = thread_p.submit(function, obj) future_list.append((obj.ip, future)) for f in future_list: result_list.append((f[0], f[1].result()[0], f[1].result()[1])) thread_p.shutdown(wait=True) failed_msg = "" for item in result_list: if not item[1]: failed_msg += f"{item[0]}: (execute_flag: {item[1]}; execute_msg: {item[2]})" if failed_msg: return False, failed_msg return True, "success" def delete_all_omp_agent(self): """清理所有omp agent(salt and monitor)""" _uninstall_flag, _uninstall_msg = self.execute_uninstall(host_obj_list=self.all_host, thread_name_prefix="uninstall_agent_", function=self.del_single_agent) ips = self.all_host.values_list("ip", flat=True) pro_obj = PrometheusUtils() write_str = [] node_path = os.path.join( pro_obj.prometheus_targets_path, "nodeExporter_all.json") for node in pro_obj.get_dic_from_yaml(node_path): if node.get("targets", [""])[0].split(":")[0] in ips: continue write_str.append(node) with open(node_path, "w") as f2: json.dump(write_str, f2, ensure_ascii=False, indent=4) time.sleep(2) reload_prometheus_url = "http://localhost:19011/-/reload" requests.post(reload_prometheus_url, auth=pro_obj.basic_auth) if not _uninstall_flag: print(_uninstall_msg) self.is_success = False self.delete_salt_key([item.ip for item in self.all_host]) return self.is_success @shared_task() def delete_hosts(host_ids): """ 执行删除异步任务 """ host_objs = Host.objects.filter(ip__in=host_ids) uninstall_objs = UninstallHosts(host_objs) uninstall_objs.delete_all_omp_agent() host_objs.delete() Service.objects.filter(ip__in=host_ids).delete() Alert.objects.filter(alert_host_ip__in=host_ids).delete() ================================================ FILE: omp_server/hosts/urls.py ================================================ # -*- coding: utf-8 -*- # Project: urls # Author: jon.liu@yunzhihui.com # Create time: 2021-09-12 11:44 # IDE: PyCharm # Version: 1.0 # Introduction: from rest_framework.routers import DefaultRouter from hosts.views import ( HostListView, HostDetailView, HostUpdateView, HostFieldCheckView, IpListView, HostMaintenanceView, HostAgentRestartView, HostOperateLogView, HostBatchValidateView, HostBatchImportView, HostInitView, HostsAgentStatusView, HostReinstallView, MonitorReinstallView, HostUninstallView ) router = DefaultRouter() router.register("hosts", HostListView, basename="hosts") router.register("hosts", HostUpdateView, basename="hosts") router.register("hostsDetail", HostDetailView, basename="hostsDetail") router.register("fields", HostFieldCheckView, basename="fields") router.register("ips", IpListView, basename="ips") router.register("maintain", HostMaintenanceView, basename="maintain") router.register("restartHostAgent", HostAgentRestartView, basename="restartHostAgent") router.register("operateLog", HostOperateLogView, basename="operateLog") router.register("batchValidate", HostBatchValidateView, basename="batchValidate") router.register("batchImport", HostBatchImportView, basename="batchImport") router.register("hostInit", HostInitView, basename="hostInit") router.register("hostsAgentStatus", HostsAgentStatusView, basename="hostsAgentStatus") router.register("hostReinstall", HostReinstallView, basename="hostReinstall") router.register("monitorReinstall", MonitorReinstallView, basename="monitorReinstall") router.register("hostUninstall", HostUninstallView, basename="hostUninstall") ================================================ FILE: omp_server/hosts/views.py ================================================ """ 主机相关视图 """ import logging from django.db import transaction from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ( ListModelMixin, CreateModelMixin, UpdateModelMixin, RetrieveModelMixin ) from rest_framework.filters import OrderingFilter from rest_framework.response import Response from rest_framework.exceptions import ValidationError from django_filters.rest_framework.backends import DjangoFilterBackend from db_models.models import (Env, Host, HostOperateLog) from utils.plugin.crypto import AESCryptor from utils.common.paginations import PageNumberPager from hosts.tasks import insert_host_celery_task from hosts.hosts_filters import (HostFilter, HostOperateFilter) from hosts.hosts_serializers import ( HostSerializer, HostMaintenanceSerializer, HostFieldCheckSerializer, HostAgentRestartSerializer, HostOperateLogSerializer, HostBatchValidateSerializer, HostBatchImportSerializer, HostDetailSerializer, HostInitSerializer, HostsAgentStatusSerializer, MonitorReinstallSerializer, HostUninstallSerializer, HostReinstallSerializer ) from promemonitor.prometheus import Prometheus from promemonitor.grafana_url import explain_url from utils.common.exceptions import OperateError from utils.common.views import BaseDownLoadTemplateView logger = logging.getLogger("server") class HostListView(GenericViewSet, ListModelMixin, CreateModelMixin): """ list: 查询主机列表 create: 创建一个新主机 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostSerializer pagination_class = PageNumberPager # 过滤,排序字段 filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = HostFilter ordering_fields = ("ip", "host_agent", "monitor_agent", "service_num", "alert_num") # 动态排序字段 dynamic_fields = ("cpu_usage", "mem_usage", "root_disk_usage", "data_disk_usage") # 操作描述信息 get_description = "查询主机" post_description = "创建主机" def list(self, request, *args, **kwargs): # 获取序列化数据列表 queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer( self.paginate_queryset(queryset), many=True) serializer_data = serializer.data # 主机密码解密 for host_info in serializer_data: aes_crypto = AESCryptor() password = aes_crypto.decode(host_info.get("password")) # 密码返回 base64 编码结果 import base64 host_info["password"] = base64.b64encode(password.encode()) # 获取监控及日志的url serializer_data = explain_url(serializer_data, is_host=True) # 实时获取主机动态 prometheus_obj = Prometheus() serializer_data = prometheus_obj.get_host_info(serializer_data) # 获取请求中 ordering 字段 query_field = request.query_params.get("ordering", "") reverse_flag = False if query_field.startswith("-"): reverse_flag = True query_field = query_field[1:] # 若排序字段在类视图 dynamic_fields 中,则对根据动态数据进行排序 none_ls = list(filter( lambda x: x.get(query_field) is None, serializer_data)) exists_ls = list(filter( lambda x: x.get(query_field) is not None, serializer_data)) if query_field in self.dynamic_fields: exists_ls = sorted( exists_ls, key=lambda x: x.get(query_field), reverse=reverse_flag) exists_ls.extend(none_ls) return self.get_paginated_response(exists_ls) class HostReinstallView(GenericViewSet, CreateModelMixin): """ create: 重装主机Agent接口 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostReinstallSerializer # 操作信息描述 post_description = "重装主机Agent" class MonitorReinstallView(GenericViewSet, CreateModelMixin): """ create: 重装监控Agent接口 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = MonitorReinstallSerializer # 操作信息描述 post_description = "重装监控Agent" class HostUninstallView(GenericViewSet, CreateModelMixin): """ create: 卸载Agent接口 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostUninstallSerializer # 操作信息描述 post_description = "卸载主机agent" class HostDetailView(GenericViewSet, RetrieveModelMixin): """ read: 查询主机详情 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostDetailSerializer # 操作描述信息 get_description = "查询主机详情" class HostUpdateView(GenericViewSet, UpdateModelMixin): """ update: 更新一个主机 partial_update: 更新一个现有主机的一个或多个字段 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostSerializer # 操作描述信息 put_description = patch_description = "更新主机" class HostFieldCheckView(GenericViewSet, CreateModelMixin): """ create: 验证主机 instance_name/ip 字段重复性 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostFieldCheckSerializer # 操作信息描述 post_description = "校验主机字段重复性" def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) return Response(serializer.is_valid()) class IpListView(GenericViewSet, ListModelMixin): """ list: 查询所有 IP 列表 """ queryset = Host.objects.filter( is_deleted=False).values_list("ip", flat=True) # 操作信息描述 get_description = "查询主机" def list(self, request, *args, **kwargs): return Response(list(self.get_queryset())) class HostMaintenanceView(GenericViewSet, CreateModelMixin): """ create: 主机进入 / 退出维护模式 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostMaintenanceSerializer # 操作信息描述 post_description = "修改主机维护模式" class HostAgentRestartView(GenericViewSet, CreateModelMixin): """ create: 主机重启Agent接口 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostAgentRestartSerializer # 操作信息描述 post_description = "重启主机Agent" class HostOperateLogView(GenericViewSet, ListModelMixin): """ list: 主机操作记录 """ queryset = HostOperateLog.objects.all() serializer_class = HostOperateLogSerializer # 过滤,排序字段 filter_backends = (DjangoFilterBackend,) filter_class = HostOperateFilter # 操作信息描述 get_description = "查询主机操作记录" class HostBatchValidateView(BaseDownLoadTemplateView, CreateModelMixin): """ list: 获取主机批量导入模板 create: 主机数据批量验证 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostBatchValidateSerializer # 操作描述信息 get_description = "获取主机批量导入模板" post_description = "主机数据批量验证" def list(self, request, *args, **kwargs): return super(HostBatchValidateView, self).list( request, template_file_name="import_hosts_template.xlsx", *args, **kwargs) def create(self, request, *args, **kwargs): ips = Host.objects.all().values_list("ip", flat=True) request_data = {"host_list": []} repeat_data = [] for host in request.data.get("host_list"): if not host.get("ip") in ips: request_data["host_list"].append(host) else: host["init_host"] = True repeat_data.append(host) if len(request_data["host_list"]) == 0: return Response({"correct": repeat_data, "error": []}) serializer = self.get_serializer(data=request_data) if not serializer.is_valid(): logger.error(f"host batch validate failed:{request.data}") raise ValidationError("数据格式错误") serializer.validated_data.get("result_dict", {}).get( "correct", []).extend(repeat_data) return Response(serializer.validated_data.get("result_dict")) class HostBatchImportView(GenericViewSet, CreateModelMixin): """ create: 主机批量添加 """ serializer_class = HostBatchImportSerializer # 操作描述信息 post_description = "主机批量添加" def create(self, request, *args, **kwargs): # 信任数据,只进行格式校验 serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): logger.error(f"host batch import failed:{request.data}") raise ValidationError("数据格式错误") try: # 主机、操作记录数据入库 default_env = Env.objects.filter(id=1).first() with transaction.atomic(): # 主机初始化信息,批量创建过程中无 id,故以 ip 作为键 host_init_info = {} host_objs = [] ips = Host.objects.all().values_list("ip", flat=True) for host in serializer.data.get("host_list"): if host.get("ip") in ips: continue # 若存在行号、运行用户则删除 if "row" in host: host.pop("row") if "run_user" in host: host.pop("run_user") host_init_info[host.get("ip")] = host.pop( "init_host", False) password = host.pop("password") host_objs.append(Host( password=AESCryptor().encode(password), agent_dir=host.get("data_folder"), env=default_env, **host, )) Host.objects.bulk_create(host_objs) # bulk_create 不返回 id,需重查获取 instance_name_list = list( map(lambda x: x.instance_name, host_objs)) host_instances = Host.objects.filter( instance_name__in=instance_name_list) operate_log_objs = [] for instance in host_instances: operate_log_objs.append(HostOperateLog( username=request.user.username, description="创建主机", host=instance, )) # 下发异步 celery 任务 insert_host_celery_task.delay( instance.id, init=host_init_info.get(instance.ip)) HostOperateLog.objects.bulk_create(operate_log_objs) except Exception as err: logger.error(f"batch import host err: {err}") import traceback logger.error(traceback.print_exc()) raise OperateError("批量导入主机失败") return Response("添加成功") class HostInitView(BaseDownLoadTemplateView, CreateModelMixin): """ create: 主机初始化 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostInitSerializer # 操作描述信息 get_description = "应用商店下载组件模板" # 操作信息描述 post_description = "主机初始化" def list(self, request, *args, **kwargs): return super(HostInitView, self).list( request, template_file_name="init_host.py", parent_path="_modules", *args, **kwargs) class HostsAgentStatusView(GenericViewSet, CreateModelMixin): """ create: 主机agent状态查询 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = HostsAgentStatusSerializer # 操作信息描述 post_description = "主机agent状态查询" def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) if not serializer.is_valid(): logger.error(f"hosts agent status failed:{request.data}") raise ValidationError("数据格式错误") ip_set = set(serializer.data.get("ip_list")) agent_online_ip_set = set(self.get_queryset().filter( ip__in=ip_set, host_agent=Host.AGENT_RUNNING ).values_list("ip", flat=True)) return Response(ip_set == agent_online_ip_set) ================================================ FILE: omp_server/inspection/__init__.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 6:00 下午 # Description: ================================================ FILE: omp_server/inspection/admin.py ================================================ from django.contrib import admin # Register your models here. from db_models.models import InspectionHistory admin.site.register([InspectionHistory]) ================================================ FILE: omp_server/inspection/apps.py ================================================ from django.apps import AppConfig class InspectionConfig(AppConfig): name = 'inspection' ================================================ FILE: omp_server/inspection/filters.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 9:02 下午 # Description: 巡检查询 import django_filters from django_filters.rest_framework import FilterSet from db_models.models import ( InspectionHistory, InspectionCrontab, InspectionReport) class InspectionHistoryFilter(FilterSet): """ 巡检历史记录过滤类 """ inspection_name = django_filters.CharFilter( help_text="报告名称:模糊搜索", field_name="inspection_name", lookup_expr="icontains") inspection_type = django_filters.CharFilter( help_text="报告类型:service、host、deep", field_name="inspection_type", lookup_expr="icontains") execute_type = django_filters.CharFilter( help_text="执行方式:手动-man;定时:auto", field_name="execute_type", lookup_expr="icontains") inspection_status = django_filters.NumberFilter( help_text="执行结果:1-进行中;2-成功;3-失败", field_name="inspection_status", lookup_expr="icontains") class Meta: model = InspectionHistory fields = ("inspection_type", "execute_type", "inspection_status") class InspectionCrontabFilter(FilterSet): """ 巡检任务配置过滤类 """ job_type = django_filters.CharFilter( help_text="任务类型:0-深度分析 1-主机巡检 2-组建巡检", field_name="job_type", lookup_expr="icontains") class Meta: model = InspectionCrontab fields = ("job_type",) class InspectionReportFilter(FilterSet): """ 巡检报告过滤类 """ inst_id = django_filters.CharFilter( help_text="巡检记录历史表id", field_name="inst_id", lookup_expr="icontains") class Meta: model = InspectionReport fields = ("inst_id",) ================================================ FILE: omp_server/inspection/get_prometheus_risk_data.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 11:53 上午 # Description: from db_models.models import Host, Service, ApplicationHub from utils.prometheus.prometheus import Prometheus def get_risk_data(handle, hosts, services): """ 查询 prometheus 异常数据 :handle: deep host service :hosts: [] :services: [] """ risks = Prometheus().query_alerts() hosts = hosts if hosts else [] services = services if services else [] host_list = [] service_list = [] _host = Host.objects _service = Service.objects app_name = list(ApplicationHub.objects.filter( id__in=services).values_list('app_name', flat=True)) for i in risks: if handle in ['host', 'deep']: # 主机 if handle == 'host' and\ i.get('labels').get('instance') not in hosts: continue if i.get('labels').get('job') == 'nodeExporter': _ = _host.filter(ip=i.get('labels').get('instance')).first() tmp = {'host_ip': i.get('labels').get('instance'), 'resolve_info': "-", 'risk_describe': i.get('annotations').get('description'), 'risk_level': i.get('labels').get('severity'), 'system': _.operate_system if _ else '-'} host_list.append(tmp) if handle in ['service', 'deep']: # 组件 if handle == 'service' and \ i.get('labels').get('job').replace('Exporter', '') \ not in app_name: continue if i.get('labels').get('job') != 'nodeExporter': tmp = {'host_ip': i.get('labels').get('instance'), 'resolve_info': "-", 'risk_describe': i.get('annotations').get('description'), 'risk_level': i.get('labels').get('severity'), 'service_name': i.get('labels').get('job'), 'service_port': '-'} service_list.append(tmp) risk_num = len(host_list) + len(service_list) return risk_num, {'host_list': host_list, 'service_list': service_list} ================================================ FILE: omp_server/inspection/get_service_topology.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 2:04 下午 # Description: from db_models.models import Service def get_topology_data(): """ 获取服务平面图数据 "return :" [{'host_ip': 'ip', 'service_list': ['redis', 'mysql']}......] """ topologies = dict() _ = Service.objects.all() for i in _: if i.ip in topologies: topologies[i.ip]['service_list'].append(i.service_instance_name) else: topologies[i.ip] = \ {'host_ip': i.ip, 'service_list': [i.service_instance_name]} return list(topologies.values()) ================================================ FILE: omp_server/inspection/inspection_utils.py ================================================ import logging import traceback from db_models.models import InspectionHistory, InspectionReport from inspection.joint_json_report import joint_json_data from utils.plugin.send_email import ModelSettingEmailBackend, many_send from utils.prometheus.create_html_tar import create_html_tar from utils.plugin import send_email as send_email_module logger = logging.getLogger("server") def send_email(inspection, emails): """ 发送邮件 :param inspection: 巡检对象 :param emails: 邮箱列表 :return: """ if not inspection: return False, "无巡检对象" inspection_report = InspectionReport.objects.filter( inst_id=inspection.id).first() if not inspection_report.file_name: return False, "未找到巡检报告!" inspection.send_email_result = inspection.ING inspection.save() reason = "" try: connection = ModelSettingEmailBackend() if inspection.inspection_type == "host" or inspection.inspection_type == "service": content_name = "SendNormalInspectionEmailContent" elif inspection.inspection_type == "deep": content_name = "SendDeepInspectionEmailContent" else: content_name = "SendEmailContent" content = getattr(send_email_module, content_name)(inspection) fail_user = many_send(connection, content, emails) except Exception as e: logger.error(f"发送邮件失败, 错误信息:{str(e)}") reason = "系统异常,请重试!" fail_user = emails if fail_user: inspection.send_email_result = inspection.FAIL reason = "发件失败,请检查smtp邮箱服务器配置!" else: inspection.send_email_result = inspection.SUCCESS inspection.email_fail_reason = reason inspection.save() return not bool(fail_user), reason # def create_inspection_html(inspection): # """ # 生成巡检报告文件、更新巡检对象信息(重复,不修改发送邮件状态) # :param inspection: # :return: # """ # if not inspection: # return False, "无巡检对象" # if inspection.inspection_status != 2: # return False, "巡检结果未成功!" # report_data = inspection.report_data # time_str = inspection.inspection_name.split("-")[1] # new_html_dir_name = f"{inspection.__class__.__name__.lower()}-{time_str}" # try: # state, result = create_html_tar(new_html_dir_name, report_data) # except Exception as e: # logger.error(f"打包巡检报告发生错误:{str(e)}, 详情为:\n{traceback.format_exc()}") # inspection.email_fail_reason = "打包巡检报告发生错误!" # inspection.save() # return False, "打包巡检报告发生错误!" # if not state: # inspection.email_fail_reason = result # inspection.save() # return False, result # inspection.file_name = result # inspection.save() # return True, result def create_send_inspection_html(inspection): """ 生成巡检报告文件、更新巡检对象信息(更新发送邮件状态) :param inspection: :return: """ if not inspection: return False, "无巡检对象" if inspection.inspection_status != 2: return False, "巡检结果未成功!" inspection_report = InspectionReport.objects.filter(inst_id=inspection.id) if not inspection_report: return False, "未找到巡检报告!" report_data = joint_json_data( inspection.inspection_type, inspection_report, inspection) time_str = inspection.inspection_name.split("-")[1] new_html_dir_name = f"{inspection.__class__.__name__.lower()}-{time_str}" try: state, result = create_html_tar(new_html_dir_name, report_data) except Exception as e: logger.error(f"打包巡检报告发生错误:{str(e)}, 详情为:\n{traceback.format_exc()}") inspection.send_email_result = 0 inspection.email_fail_reason = "打包巡检报告发生错误!" inspection.save() return False, "打包巡检报告发生错误!" if not state: inspection.send_email_result = 0 inspection.email_fail_reason = result inspection.save() return False, result inspection_report.file_name = result inspection.save() inspection_report.save() return True, result def send_report_email(inspection_module, inspection_id, emails): """ 生成、发送报告 :param inspection_module: DeepInspection、NormalInspection :param inspection_id: :param emails: 邮箱list :return: """ inspection = InspectionHistory.objects.filter(id=inspection_id).first() if not inspection: return False, "未找到对应的巡检!" if inspection.inspection_status != 2: return False, "巡检结果未成功!" inspection_report = InspectionReport.objects.filter( inst_id=inspection_id).first() if not inspection_report: return False, "未找到对应的巡检报告!" if inspection.send_email_result == 2: return False, "正在发送巡检报告,请稍后再试!" inspection.send_email_result = 2 inspection.save() if not inspection_report.file_name: try: state, result = create_send_inspection_html(inspection) except Exception as e: logger.error( f"打包巡检报告发生错误:{str(e)}, 详情为:\n{traceback.format_exc()}") inspection.send_email_result = inspection.FAIL inspection.email_fail_reason = "打包巡检报告发生错误!" inspection.save() return False, "打包巡检报告发生错误!" if not state: return False, result try: state, email_fail_reason = send_email(inspection, emails) except Exception as e: logger.error(f"发送邮件发生错误:{str(e)}, 详情为:\n{traceback.format_exc()}") return "发送邮件发生错误!" return state, email_fail_reason ================================================ FILE: omp_server/inspection/joint_json_report.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/25 10:30 上午 # Description: def joint_json_data(handle, _r, _h): """ 巡检导出、报告展示时,组装数据结构 """ ret = { "summary": { "task_info": { "task_name": _h.inspection_name, "operator": _h.inspection_operator, "task_status": _h.inspection_status }, "time_info": { "start_time": _h.start_time.strftime('%Y-%m-%d %H:%M:%S'), "end_time": _h.end_time.strftime('%Y-%m-%d %H:%M:%S') if _h.end_time else '', "cost": _h.duration }, "scan_info": _r.scan_info, "scan_result": _r.scan_result }, "risks": _r.risk_data if _r.risk_data else {"host_list": [], "service_list": []}, "detail_dict": {}, "file_name": _r.file_name } # 主机巡检 if handle == 'host': ret['detail_dict']['host'] = _r.host_data # 组件巡检 if handle == 'service': ret['detail_dict']['component'] = _r.serv_data # 深度巡检 if handle == 'deep': # 服务平面图 ret["service_topology"] = _r.serv_plan if _r.serv_plan else [] ret['detail_dict']['host'] = _r.host_data ret['detail_dict']["service"] = _r.serv_data ret['detail_dict']["database"] = [] ret['detail_dict']["component"] = [] return ret ================================================ FILE: omp_server/inspection/serializers.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 8:36 下午 # Description: 巡检序列化 from rest_framework.serializers import (ModelSerializer,) from db_models.models import ( InspectionHistory, InspectionCrontab, InspectionReport) class InspectionHistorySerializer(ModelSerializer): """ 巡检记录历史表 """ class Meta: """ 元数据 """ model = InspectionHistory fields = "__all__" class InspectionCrontabSerializer(ModelSerializer): """ 巡检任务 配置表 """ class Meta: """ 元数据 """ model = InspectionCrontab fields = "__all__" class InspectionReportSerializer(ModelSerializer): """ 巡检报告表 """ class Meta: """ 元数据 """ model = InspectionReport fields = "__all__" ================================================ FILE: omp_server/inspection/tasks.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 6:06 下午 # Description: 巡检异步任务及定时任务 import logging import traceback from datetime import datetime from celery import shared_task from inspection.inspection_utils import send_email from utils.prometheus.thread import MyThread from celery.utils.log import get_task_logger from db_models.models import Host, Env, Service, ModuleSendEmailSetting from db_models.models import InspectionHistory, InspectionReport from utils.prometheus.prometheus import back_fill from utils.prometheus.target_host import target_host_thread from utils.prometheus.target_service import target_service_run from utils.prometheus.create_html_tar import create_html_tar from inspection.joint_json_report import joint_json_data from inspection.get_prometheus_risk_data import get_risk_data from inspection.get_service_topology import get_topology_data # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") def get_hosts_data(env, hosts): """ 查询多主机prometheus数据,组装后进行反填 :env: 环境queryset :hosts: 主机列表,例:["主机ip"] """ temp_list = list() threads = list() total_no = error_no = 0 # 总指标数/异常指标数 for instance in hosts: total_no += 23 # 总指标数;当前共23个 threads.append(MyThread(func=target_host_thread, args=(env, instance))) for t in threads: t.start() for t in threads: t.join() # 用join等待线程执行结束 temp_list.append(t.res) # 组装每个线程的返回值 scan_result = { "all_target_num": total_no, "abnormal_target": error_no, "healthy": "" } scan_info = {"host": len(hosts), "service": 0, "component": 0} # 扫描统计 return scan_info, scan_result, temp_list @shared_task def get_prometheus_data(env_id, hosts, services, history_id, report_id, handle): """ 异步任务:查询多巡检类型prometheus数据,组装后进行反填 :env_id: 环境,例:Env id :hosts: 主机列表,例:["主机ip"] :services: 服务列表,例:[8] :history_id: 巡检历史表id,例:1 :report_id: 巡检报告表id,例:1 :handle: 巡检类型 service-服务巡检、host-主机巡检、deep-深度巡检 """ try: file_name = '' # 导出文件名 env = Env.objects.filter(id=env_id).first() _h = InspectionHistory.objects.filter(id=history_id).first() kwargs = {'history_id': history_id, 'report_id': report_id} if handle == 'host': # 主机巡检 file_name = f"hostinspection{_h.inspection_name.split('-')[1]}" scan_info, scan_result, host_data = get_hosts_data(env, hosts) kwargs.update({ 'scan_info': scan_info, 'scan_result': scan_result, 'host_data': host_data, 'file_name': f"{file_name}.tar.gz"}) elif handle == 'service': # 组件巡检 file_name = f"serviceinspection{_h.inspection_name.split('-')[1]}" scan_info, scan_result, serv_data = \ target_service_run(env, services) kwargs.update({ 'scan_info': scan_info, 'scan_result': scan_result, 'serv_data': serv_data, 'file_name': f"{file_name}.tar.gz"}) elif handle == 'deep': # 主机巡检 file_name = f"deepinspection{_h.inspection_name.split('-')[1]}" hosts = Host.objects.filter( env=env.id).values_list('ip', flat=True) if len(hosts) > 0: h_info, h_result, host_data = get_hosts_data(env, list(hosts)) else: h_info, host_data = {'host': 0}, [] h_result = {'all_target_num': 0, 'abnormal_target': 0} # 组件巡检 _ = Service.objects.filter( service__is_base_env=False).exclude( service_status__in=[5, 6, 7]) services = list(_.values_list('id', flat=True)) if len(services) > 0: s_info, s_result, serv_data = target_service_run(env, services) else: s_info, serv_data = {'service': 0}, [] s_result = {'all_target_num': 0, 'abnormal_target': 0} # 主机巡检 + 组件巡检 合并结果 scan_info = {"host": h_info.get('host'), "component": 0, "service": s_info.get('service')} all_target_num = \ h_result.get('all_target_num') + s_result.get('all_target_num') scan_result = {"all_target_num": all_target_num, "abnormal_target": 0, "healthy": "-"} kwargs.update({ 'scan_info': scan_info, 'scan_result': scan_result, 'serv_data': serv_data, 'host_data': host_data, 'file_name': f"{file_name}.tar.gz"}) # 服务平面图 kwargs['serv_plan'] = get_topology_data() # 风险指标 risk_num, risk_data = get_risk_data(handle, hosts, services) kwargs['risk_data'] = risk_data # 根据风险指标更新 kwargs['scan_result']['abnormal_target'] = risk_num # 反填巡检记录、巡检报告 数据 back_fill(**kwargs) # 打包html文件 _r = InspectionReport.objects.filter(id=report_id).first() _h = InspectionHistory.objects.filter(id=history_id).first() ret = joint_json_data(_h.inspection_type, _r, _h) create_html_tar(file_name, ret) if _h.inspection_status == 2: mses = ModuleSendEmailSetting.get_email_settings( env_id, "inspection") if not mses: return if mses.send_email == 0: return email_users = mses.to_users.split(",") if len(email_users) > 0: send_email(_h, email_users) except Exception as e: logger.error( f"Inspection man task failed with error: {traceback.format_exc(e)}") @shared_task def inspection_crontab(**kwargs): """ 巡检 定时任务,由增加及修改接口增加的celery任务调起执行 :kwargs: {"env": 1, "job_type": 1, "job_name": "主机巡检"} """ try: env = kwargs.get('env') job_type = kwargs.get('job_type') job_name = kwargs.get('job_name') # 1、查询环境是否存在 env = Env.objects.filter(id=env).first() if not env: logger.error( f"Inspection auto task failed with error: ID={env}的环境不存在") else: hosts, services = [], [] if job_type in [0, 1]: # 2、查询环境下主机信息 hosts = Host.objects.filter(env=env.id).values_list( 'ip', flat=True) if len(hosts) == 0: logger.error(f"Inspection auto task failed with error: " f"ID={env.id}环境下无主机数据") if job_type in [0, 2]: # 2、查询环境下组件信息 _ = Service.objects.filter( service__is_base_env=False ).exclude(service_status__in=[5, 6, 7]) services = list(_.values_list('id', flat=True)) if len(services) == 0: logger.error(f"Inspection auto task failed with error: " f"ID={env.id}环境下无组件数据") # job_type 与 inspection_type 参数对应 inspection_type = {0: 'deep', 1: 'host', 2: 'service'} # 3、组装巡检历史表入库数据,并存储入库 now = datetime.now() num = InspectionHistory.objects.filter( start_time__year=now.year, start_time__month=now.month, start_time__day=now.day).count() his_dict = { 'inspection_name': f"{job_name}定时巡检-{now.strftime('%Y%m%d')}{num + 1}", 'inspection_type': inspection_type.get(job_type), 'inspection_status': 1, 'execute_type': 'auto', 'inspection_operator': 'admin', 'hosts': list(hosts), 'services': list(services), 'env': env} his_obj = InspectionHistory(**his_dict) his_obj.save() # 4、组装巡检报告表数据,并存储入库 rep_obj = InspectionReport(**{'inst_id': his_obj}) rep_obj.save() # 5、查询prometheus数据,组装后进行反填 get_prometheus_data( env_id=env.id, hosts=list(hosts), services=list(services), history_id=his_obj.id, report_id=rep_obj.id, handle=inspection_type.get(job_type)) except Exception as e: logger.error(f"Inspection auto task failed with error:" f"{traceback.format_exc(e)}") ================================================ FILE: omp_server/inspection/urls.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 8:52 下午 # Description: 巡检 路由 from rest_framework.routers import DefaultRouter from inspection.views import ( InspectionHistoryView, InspectionCrontabView, InspectionReportView, InspectionServiceView, InspectionSendEmailSettingView, InspectionSendEmailAPIView) router = DefaultRouter() # 巡检 历史记录 router.register("history", InspectionHistoryView, basename="history") # 巡检-组件巡检 组件列表 router.register("services", InspectionServiceView, basename="services") # 巡检 定时任务配置 router.register("crontab", InspectionCrontabView, basename="crontab") # 巡检 报告 router.register("report", InspectionReportView, basename="report") router.register("inspectionSendEmailSetting", InspectionSendEmailSettingView, basename="inspectionSendEmailSetting") router.register("inspectionSendEmail", InspectionSendEmailAPIView, "inspectionSendEmail") ================================================ FILE: omp_server/inspection/views.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/13 6:06 下午 # Description: 巡检视图 import datetime import logging import traceback from django.core.validators import EmailValidator from rest_framework import status from rest_framework.response import Response from rest_framework.serializers import Serializer from inspection.inspection_utils import send_report_email from utils.common.paginations import PageNumberPager from rest_framework.viewsets import GenericViewSet from django_filters.rest_framework.backends import DjangoFilterBackend from rest_framework.mixins import ( ListModelMixin, CreateModelMixin, UpdateModelMixin, RetrieveModelMixin) from utils.plugin.crontab_utils import CrontabUtils from inspection.tasks import get_prometheus_data from db_models.models import ( Env, Service, InspectionHistory, InspectionCrontab, InspectionReport, ModuleSendEmailSetting) from inspection.filters import ( InspectionHistoryFilter, InspectionCrontabFilter, ) from inspection.serializers import ( InspectionHistorySerializer, InspectionCrontabSerializer, InspectionReportSerializer) from rest_framework.filters import OrderingFilter from inspection.joint_json_report import joint_json_data logger = logging.getLogger('server') class InspectionServiceView(ListModelMixin, GenericViewSet): """ list: 组件巡检 组件列表 """ def list(self, request, *args, **kwargs): # 只能是安装成功的组件 rets = list() _ = Service.objects.filter(service__is_base_env=False).exclude(service__app_port=None).exclude( service_status__in=[5, 6, 7]) for i in _: rets.append({'service__id': i.id, 'service__app_name': i.service_instance_name}) return Response(data=rets, status=status.HTTP_200_OK) class InspectionHistoryView(ListModelMixin, GenericViewSet, CreateModelMixin): """ list: 查询巡检记录历史记录列表 """ queryset = InspectionHistory.objects.all() serializer_class = InspectionHistorySerializer pagination_class = PageNumberPager # 过滤字段 filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = InspectionHistoryFilter # 动态排序字段 dynamic_fields = ("start_time",) # 操作描述信息 get_description = "查询巡检历史记录列表" post_description = "查询巡检历史记录列表" def list(self, request, *args, **kwargs): # 获取序列化数据列表 queryset = self.filter_queryset(self.get_queryset()) serializer = self.get_serializer( self.paginate_queryset(queryset), many=True) serializer_data = serializer.data # 获取请求中 ordering 字段 query_field = request.query_params.get("ordering", "") reverse_flag = False if query_field.startswith("-"): reverse_flag = True query_field = query_field[1:] # 若排序字段在类视图 dynamic_fields 中,则对根据动态数据进行排序 none_ls = list(filter( lambda x: x.get(query_field) is None, serializer_data)) exists_ls = list(filter( lambda x: x.get(query_field) is not None, serializer_data)) if query_field in self.dynamic_fields: exists_ls = sorted( exists_ls, key=lambda x: x.get(query_field), reverse=reverse_flag) exists_ls.extend(none_ls) return self.get_paginated_response(exists_ls) @staticmethod def joint_inspection_name(data_dict): now = datetime.datetime.now() num = InspectionHistory.objects.filter( start_time__year=now.year, start_time__month=now.month, start_time__day=now.day).count() tp = {'deep': '深度巡检', 'host': '主机巡检', 'service': '组件巡检'} name = f"{tp.get(data_dict.get('inspection_type'))}-" \ f"{now.strftime('%Y%m%d')}{num + 1}" return name def create(self, request, *args, **kwargs): data_dict = request.data # 一、创建巡检历史表数据 env_obj = Env.objects.filter(id=data_dict['env']).first() data_dict['env'] = env_obj data_dict['inspection_name'] = self.joint_inspection_name(data_dict) his_obj = InspectionHistory(**data_dict) his_obj.save() # 二、创建巡检记录历史表关联的报告表的数据 rep_obj = InspectionReport(**{'inst_id': his_obj}) rep_obj.save() # 三、手动序列化数据,不是json的不能response data_dict.update({'id': his_obj.id, 'env': data_dict['env'].id}) # 四、下发celery异步任务 handle = data_dict.get('inspection_type') # 异步下发 get_prometheus_data.delay( env_id=env_obj.id, hosts=his_obj.hosts, services=his_obj.services, history_id=his_obj.id, report_id=rep_obj.id, handle=handle) return Response(data_dict, status=status.HTTP_200_OK) class InspectionCrontabView(RetrieveModelMixin, ListModelMixin, GenericViewSet, CreateModelMixin, UpdateModelMixin): """ list: 查询巡检任务列表 create: 创建一个新巡检任务 update: 更新一个现有巡检任务 """ queryset = InspectionCrontab.objects.all() serializer_class = InspectionCrontabSerializer pagination_class = PageNumberPager # 过滤字段 lookup_field = 'job_type' filter_backends = (DjangoFilterBackend,) filter_class = InspectionCrontabFilter # 操作描述信息 get_description = "查询巡检任务配置列表" post_description = "新建巡检任务配置列表" put_description = "更新巡检任务配置列表" @staticmethod def transfer_week(request): """ 因前端day_of_week参数传递 不符合规范,只能适配了呗 """ day_of_week = request.data.get('crontab_detail').get('day_of_week') if day_of_week == '6': day_of_week = '0' elif day_of_week == '*': pass else: day_of_week = str(int(day_of_week) + 1) return day_of_week def create(self, request, *args, **kwargs): # 判断是否需要下发任务到celery:0-开启,1-关闭 is_success = True request.data['job_type'] = int(request.data.get('job_type')) if request.data.get('is_start_crontab') == 0: tp = {0: 'deep', 1: 'host', 2: 'service'} task_name = \ f"inspection_cron_task_{tp.get(request.data.get('job_type'))}" task_func = "inspection.tasks.inspection_crontab" cron_obj = CrontabUtils(task_name=task_name, task_func=task_func, task_kwargs=request.data) cron_args = { 'minute': request.data.get('crontab_detail').get('minute'), 'hour': request.data.get('crontab_detail').get('hour'), 'day_of_month': request.data.get('crontab_detail').get('day'), 'month_of_year': request.data.get('crontab_detail').get('month'), 'day_of_week': self.transfer_week(request) } is_success, job_name = cron_obj.create_crontab_job(**cron_args) else: pass if is_success: # 只是想在增加时加个判断及对应操作,增加还是执行父类的create return CreateModelMixin.create(self, request, *args, **kwargs) else: return Response(data='定时任务已存在,请勿重复操作', status=status.HTTP_200_OK) def update(self, request, *args, **kwargs): # 判断是否需要下发任务到celery:0-开启,1-关闭 is_success = True request.data['job_type'] = int(request.data.get('job_type')) if request.data.get('is_start_crontab') == 0: tp = {0: 'deep', 1: 'host', 2: 'service'} task_name = \ f"inspection_cron_task_{tp.get(request.data.get('job_type'))}" task_func = 'inspection.tasks.inspection_crontab' cron_obj = CrontabUtils(task_name=task_name, task_func=task_func, task_kwargs=request.data) cron_args = { 'minute': request.data.get('crontab_detail').get('minute'), 'hour': request.data.get('crontab_detail').get('hour'), 'day_of_month': request.data.get('crontab_detail').get('day'), 'month_of_year': request.data.get('crontab_detail').get('month'), 'day_of_week': self.transfer_week(request) } # 删除定时任务 cron_obj.delete_job() # 增加定时任务 is_success, job_name = cron_obj.create_crontab_job(**cron_args) else: tp = {0: 'deep', 1: 'host', 2: 'service'} task_name = \ f"inspection_cron_task_{tp.get(request.data.get('job_type'))}" task_func = 'inspection.tasks.inspection_crontab' cron_obj = CrontabUtils(task_name=task_name, task_func=task_func, task_kwargs=request.data) # 删除定时任务 cron_obj.delete_job() if is_success: # 只是想在修改时加个判断及对应操作,修改还是执行父类的update return UpdateModelMixin.update(self, request, *args, **kwargs) else: return Response(data={'code': 500, 'message': '定时任务修改失败,请重试'}, status=status.HTTP_200_OK) class InspectionReportView(GenericViewSet, RetrieveModelMixin): """ list: 查询巡检报告列表 """ queryset = InspectionReport.objects.all() serializer_class = InspectionReportSerializer # 过滤字段 lookup_field = 'inst_id' # 操作描述信息 get_description = "查询巡检任务配置列表" def retrieve(self, request, *args, **kwargs): data_dict = request.parser_context.get('kwargs') _r = InspectionReport.objects.filter( inst_id=data_dict.get('inst_id')).first() _h = InspectionHistory.objects.filter( id=data_dict.get('inst_id')).first() if not _r or not _h: return Response('巡检报告缺失,暂不可查看') ret = joint_json_data(_h.inspection_type, _r, _h) return Response(ret) class InspectionSendEmailSettingView(GenericViewSet, ListModelMixin, CreateModelMixin): """ 读写巡检邮箱收件配置 """ get_description = "读取巡检邮箱设置" post_description = "更新巡检邮箱设置" serializer_class = Serializer def list(self, request, *args, **kwargs): # env_id = request.GET.get("env_id") env_id = 1 # 单环境暂为1 email_setting = ModuleSendEmailSetting.get_email_settings( env_id, "inspection") # TODO 暂写死为巡检 if not email_setting: return Response(data={}) return Response( data={ "to_users": email_setting.to_users, "send_email": email_setting.send_email } ) def create(self, request, *args, **kwargs): env_id = request.data.get("env_id") send_email = request.data.get("send_email", False) to_users = request.data.get("to_users") if not to_users and send_email: return Response(data={"code": 1, "message": "收件邮箱必填!"}) if to_users: emails = to_users.split(",") for email in emails: try: EmailValidator()(email) except Exception as e: message = f"收件箱{email}格式错误!" logger.error(f"{message} 错误信息:{str(e)}") return Response(data={"code": 1, "message": message}) try: ModuleSendEmailSetting.update_email_settings( env_id, "inspection", send_email, to_users) # TODO 暂写死为巡检 except Exception as e: logger.info(f"更新邮箱配置失败:{str(e)},详情为:{traceback.format_exc()}") return Response(data={"code": 1, "message": "更新邮箱配置失败!"}) return Response({}) class InspectionSendEmailAPIView(GenericViewSet, CreateModelMixin): """ 巡检邮件推送 """ post_description = "巡检邮件推送" serializer_class = Serializer def create(self, request, *args, **kwargs): inspection_id = request.data.get("id") inspection_module = request.data.get("module") if inspection_module not in ("host", "service", "deep"): return Response(data={"code": 1, "message": "请选择正确的巡检对象!"}) to_users = request.data.get("to_users") if not to_users: return Response(data={"code": 1, "message": "请填入接收邮件邮箱!"}) emails = to_users.split(",") for email in emails: try: EmailValidator()(email) except Exception as e: message = f"收件箱{email}格式错误!" logger.error(f"{message} 错误信息:{str(e)}") return Response(data={"code": 1, "message": message}) state, result = send_report_email( inspection_module, inspection_id, emails) if not state: return Response(data={"code": 1, "message": result}) return Response({}) ================================================ FILE: omp_server/manage.py ================================================ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" import os import sys def main(): """Run administrative tasks.""" os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'omp_server.settings') try: from django.core.management import execute_from_command_line except ImportError as exc: raise ImportError( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" ) from exc execute_from_command_line(sys.argv) if __name__ == '__main__': main() ================================================ FILE: omp_server/omp_server/__init__.py ================================================ # This will make sure the app is always imported when # Django starts so that shared_task will use this app. import pymysql from .celery import app as celery_app __all__ = ('celery_app',) pymysql.install_as_MySQLdb() ================================================ FILE: omp_server/omp_server/asgi.py ================================================ """ ASGI config for omp_server project. It exposes the ASGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ """ import os from django.core.asgi import get_asgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'omp_server.settings') application = get_asgi_application() ================================================ FILE: omp_server/omp_server/celery.py ================================================ # -*- coding: utf-8 -*- # Project: celery # Author: jon.liu@yunzhihui.com # Create time: 2021-09-12 11:30 # IDE: PyCharm # Version: 1.0 # Introduction: """ celery相关 """ import os from celery import Celery from utils.parse_config import OMP_REDIS_HOST from utils.parse_config import OMP_REDIS_PORT from utils.parse_config import OMP_REDIS_PASSWORD # Set the default Django settings module for the 'celery' program. os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'omp_server.settings') app = Celery('omp_server') # Using a string here means the worker doesn't have to serialize # the configuration object to child processes. # - namespace='CELERY' means all celery-related configuration keys # should have a `CELERY_` prefix. app.config_from_object('django.conf:settings', namespace='CELERY') app.conf.broker_url = \ f'redis://:{OMP_REDIS_PASSWORD}@{OMP_REDIS_HOST}:{OMP_REDIS_PORT}/0' # Load task modules from all registered Django apps. app.autodiscover_tasks() # @app.task(bind=True) # def debug_task(self): # print(f'Request: {self.request!r}') ================================================ FILE: omp_server/omp_server/settings.py ================================================ """ Django settings for omp_server project. Generated by 'django-admin startproject' using Django 3.1.4. For more information on this file, see https://docs.djangoproject.com/en/3.1/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/3.1/ref/settings/ """ import os import random import datetime from pathlib import Path from utils.parse_config import OMP_MYSQL_HOST, OMP_MYSQL_PORT, \ OMP_MYSQL_USERNAME, OMP_MYSQL_PASSWORD, TOKEN_EXPIRATION, \ SSH_CMD_TIMEOUT, PRIVATE_KEY SSH_CMD_TIMEOUT = SSH_CMD_TIMEOUT PRIVATE_KEY = PRIVATE_KEY # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent PROJECT_DIR = os.path.dirname(BASE_DIR) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'rofvdj3gbyg0(vb-ck=d(*1o=jx=l2_%c0*ox^rv%2s36(u3-@' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True # 允许所有 ALLOWED_HOSTS = ["*"] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'django_celery_results', 'django_celery_beat', 'rest_framework', 'db_models', 'users', 'tests', 'inspection', 'service_upgrade', 'tool', ] MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'utils.middleware_handler.RoleAuthenticationMiddleware', 'utils.middleware_handler.OperationLogMiddleware' ] ROOT_URLCONF = 'omp_server.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ] WSGI_APPLICATION = 'omp_server.wsgi.application' # Database # https://docs.djangoproject.com/en/3.1/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'omp', 'USER': OMP_MYSQL_USERNAME, 'PASSWORD': OMP_MYSQL_PASSWORD, 'HOST': OMP_MYSQL_HOST, 'PORT': int(OMP_MYSQL_PORT), 'TEST': { 'CHARSET': 'utf8', 'COLLATION': 'utf8_general_ci', "NAME": f"test_omp_{random.randint(100, 200)}" }, 'OPTIONS': { 'init_command': 'SET sql_mode=STRICT_TRANS_TABLES', } } } # Password validation # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.' 'UserAttributeSimilarityValidator', }, { 'NAME': 'django.contrib.auth.password_validation.' 'MinimumLengthValidator', }, { 'NAME': 'django.contrib.auth.password_validation.' 'CommonPasswordValidator', }, { 'NAME': 'django.contrib.auth.password_validation.' 'NumericPasswordValidator', }, ] # Internationalization # https://docs.djangoproject.com/en/3.1/topics/i18n/ LANGUAGE_CODE = 'en-us' USE_I18N = True USE_L10N = True TIME_ZONE = 'Asia/Shanghai' USE_TZ = False # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.1/howto/static-files/ STATIC_URL = '/static/' STATIC_ROOT = os.path.join(BASE_DIR, "static/") # DRF 相关设置 REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_jwt.authentication.JSONWebTokenAuthentication", ), "EXCEPTION_HANDLER": "utils.exception_handler.common_exception_handler", "DEFAULT_RENDERER_CLASSES": ( "utils.response_handler.APIRenderer", ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", ), "DEFAULT_SCHEMA_CLASS": "rest_framework.schemas.coreapi.AutoSchema", } # JWT相关设置 JWT_AUTH = { "JWT_EXPIRATION_DELTA": datetime.timedelta(days=int(TOKEN_EXPIRATION)), "JWT_ALLOW_REFRESH": True, "JWT_AUTH_COOKIE": "jwtToken", } AUTH_USER_MODEL = "db_models.UserProfile" # celery 相关配置 CELERY_RESULT_BACKEND = "django-db" CELERY_ENABLE_UTC = False CELERY_TIMEZONE = TIME_ZONE DJANGO_CELERY_BEAT_TZ_AWARE = False CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_IMPORTS = ("hosts.tasks", "inspection.tasks") LOGGER_CLASS = 'concurrent_log_handler.ConcurrentRotatingFileHandler' LOG_BACKUP_SIZE = 1024 * 1024 * 100 LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'standard': { 'format': '[%(asctime)s][%(levelname)s] %(pathname)s %(lineno)d -> %(message)s'} }, 'filters': { }, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'class': 'django.utils.log.AdminEmailHandler', 'include_html': True, }, 'default': { 'level': 'DEBUG', 'class': LOGGER_CLASS, 'filename': os.path.join(PROJECT_DIR, "logs/debug.log"), # 日志输出文件 'maxBytes': LOG_BACKUP_SIZE, # 文件大小 'backupCount': 5, # 备份份数 'formatter': 'standard', # 使用哪种formatters日志格式 }, 'error': { 'level': 'ERROR', 'class': LOGGER_CLASS, 'filename': os.path.join(PROJECT_DIR, "logs/error.log"), 'maxBytes': LOG_BACKUP_SIZE, 'backupCount': 5, 'formatter': 'standard', }, 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'standard' }, 'request_handler': { 'level': 'DEBUG', 'class': LOGGER_CLASS, 'filename': os.path.join(PROJECT_DIR, "logs/request.log"), 'maxBytes': LOG_BACKUP_SIZE, 'backupCount': 5, 'formatter': 'standard', }, }, 'loggers': { 'django': { 'handlers': ['request_handler'], 'level': 'INFO', 'propagate': True, }, 'server': { 'handlers': ['default', 'error'], 'level': "INFO", 'propagate': True } } } # DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # grafana跳转使用 X_FRAME_OPTIONS = 'SAMEORIGIN' # 发邮件使用,是否使用ssl,使用端口:465/994 不使用:25 EMAIL_USE_SSL = True # 可定制化指标的服务及指标 CUSTOM_THRESHOLD_SERVICES = { "kafka": {"kafka_consumergroup_lag"} } # 备份相关配置 # 可备份的组件 BACKUP_DEFAULT_PATH = os.path.join(PROJECT_DIR, "data/backup/") SCAN_TOOL_LOCK_KEY = "tool_package_verify" INTERFACE_KINDS = {"/api/appStore/upload/": "修改", "/api/appStore/remove/": "删除", "/api/appStore/publish/": "修改", "/api/appStore/executeLocalPackageScan/": "修改", "/api/appStore/deploymentPlanValidate/": "查看", "/api/appStore/deploymentPlanImport/": "增加", "/api/appStore/createInstallInfo/": "增加", "/api/appStore/executeInstall/": "增加", "/api/appStore/checkInstallInfo/": "查看", "/api/appStore/createServiceDistribution/": "增加", "/api/appStore/checkServiceDistribution/": "查看", "/api/appStore/createInstallPlan/": "新增", "/api/appStore/createComponentInstallInfo/": "新增", "/api/appStore/retryInstall/": "修改", "/api/backups/backupSettings/": "修改", "/api/backups/backupOnce/": "新增", "/api/backups/backupHistory/": "删除", "/api/backups/backupSendEmail/": "新增", "/api/hosts/hosts/": "修改", "/api/hosts/fields/": "查看", "/api/hosts/maintain/": "修改", "/api/hosts/restartHostAgent/": "修改", "/api/hosts/batchValidate/": "查看", "/api/hosts/batchImport/": "新增", "/api/hosts/hostInit/": "修改", "/api/hosts/hostsAgentStatus/": "查询", "/api/hosts/hostReinstall/": "修改", "/api/hosts/monitorReinstall/": "修改", "/api/inspection/history/": "查询", "/api/inspection/crontab/": "新增", "/api/inspection/inspectionSendEmailSetting/": "修改", "/api/inspection/inspectionSendEmail/": "查询", "/api/promemonitor/monitorurl/": "修改", "/api/promemonitor/updateAlert/": "修改", "/api/promemonitor/restartMonitorAgent/": "修改", "/api/promemonitor/globalMaintain/": "修改", "/api/promemonitor/receiveAlert/": "新增", "/api/promemonitor/updateSendEmailConfig/": "修改", "/api/promemonitor/updateSendAlertSetting/": "新增", "/api/promemonitor/hostThreshold/": "修改", "/api/promemonitor/serviceThreshold/": "修改", "/api/promemonitor/customThreshold/": "修改", "/api/upgrade/do-upgrade/": "修改", "/api/rollback/do-rollback/": "修改", "/api/services/action/": "修改", "/api/services/delete/": "查询", "/api/services/SelfHealingSetting/": "修改", "/api/services/UpdateSelfHealingHistory/": "修改", "/api/users/users/": "新增", "/api/users/updatePassword/": "修改" } # for automated testing DATA_JSON_SECRET = "Yunweiguanli@OMP_123" ================================================ FILE: omp_server/omp_server/urls.py ================================================ """omp_server URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/3.1/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: path('', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.urls import include, path 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin from django.urls import path, include, re_path # coreAPI documentation from rest_framework.documentation import include_docs_urls from rest_framework.permissions import AllowAny from rest_framework.authentication import ( SessionAuthentication, BasicAuthentication ) from users.urls import router as users_router from users.views import JwtAPIView from hosts.urls import router as hosts_router from app_store.urls import router as app_store_router from promemonitor.urls import router as promemonitor_router from promemonitor.grafana_views import grafana_proxy_view from inspection.urls import router as router_inspection from services.urls import urlpatterns as services_urlpatterns from backups.urls import router as backups_router from tool.urls import urlpatterns as tool_urlpatterns from utils.common.urls import router as common_router from service_upgrade.urls import upgrade_urlpatterns, rollback_urlpatterns urlpatterns_inside = [ path("login/", JwtAPIView.as_view(), name="login"), path("users/", include(users_router.urls), name="users"), path("hosts/", include(hosts_router.urls), name="hosts"), path("promemonitor/", include(promemonitor_router.urls), name="promemonitor"), path("appStore/", include(app_store_router.urls), name="appStore"), path('inspection/', include(router_inspection.urls), name="inspection"), path("services/", include(services_urlpatterns), name="services"), path("backups/", include(backups_router.urls), name="backups"), path("upgrade/", include(upgrade_urlpatterns), name="upgrade"), path("rollback/", include(rollback_urlpatterns), name="rollback"), path("tool/", include(tool_urlpatterns), name="tool"), path("common/", include(common_router.urls), name="common"), ] urlpatterns = [ path("admin/", admin.site.urls), path("api/", include(urlpatterns_inside)), path("docs/", include_docs_urls( title="API 接口文档", authentication_classes=( SessionAuthentication, BasicAuthentication), permission_classes=(AllowAny,), ), name="docs"), re_path(r'^proxy/v1/grafana/(?P.*)', grafana_proxy_view), ] ================================================ FILE: omp_server/omp_server/wsgi.py ================================================ """ WSGI config for omp_server project. It exposes the WSGI callable as a module-level variable named ``application``. For more information on this file, see https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ """ import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'omp_server.settings') application = get_wsgi_application() ================================================ FILE: omp_server/promemonitor/__init__.py ================================================ ================================================ FILE: omp_server/promemonitor/admin.py ================================================ from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/promemonitor/alert_util.py ================================================ import datetime import logging import traceback import pytz from omp_server.settings import TIME_ZONE from db_models.models import Host, Service, ApplicationHub from promemonitor.grafana_url import explain_url logger = logging.getLogger('server') def get_service_type(ip, service_name): """ 获取服务的中文名称以及服务分类 :param ip: ip :param service_name: 服务名称 :type service_name str :return: product_cn_name, service_type """ product_cn_name = service_name service_type = ip return product_cn_name, service_type def get_monitor_url(ele): try: monitor_url = explain_url(ele)[0].get('monitor_url') except TypeError: logger.error('get monitor url failed') monitor_url = None return monitor_url def get_log_url(ele): try: monitor_log_url = explain_url(ele)[0].get('log_url') except TypeError: logger.error('get monitor log url failed') monitor_log_url = None return monitor_log_url def utc_to_local(utc_time_str, utc_format='%Y-%m-%dT%H:%M:%SZ'): """ 时区转换,如果转换报错,那么使用当前时间作为返回值 :type utc_time_str str :param utc_time_str: utc时间字符串 :type utc_format str :param utc_format: utc时间格式 :return: """ try: utc_time_str = utc_time_str.split( ".")[0] + utc_time_str.split(".")[-1][-1] local_tz = pytz.timezone(TIME_ZONE) local_format = "%Y-%m-%d %H:%M:%S" utc_dt = datetime.datetime.strptime(utc_time_str, utc_format) local_dt = utc_dt.replace(tzinfo=pytz.utc).astimezone(local_tz) time_str = local_dt.strftime(local_format) return time_str except Exception as e: logger.error(f"在转化时间格式时报错: {str(e)}\n详情为: {traceback.format_exc()}") return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") class AlertAnalysis: def __init__(self, item): """ 解析prometheus返回的信息 :param item: { "status": "resolved", "labels": { "alertname":"app state", "app":"nbdServer", "instance":"10.0.9.61:18215", "ip":"10.0.9.61", "job":"10.0.9.61", "severity":"critical" }, "annotations": { "consignee":"2871253303@qq.com", "description":"主机 10.0.12.35 中的 zookeeper_exporter 已经down掉超过一分钟.", "summary":"app state(instance 10.0.9.61:18215)" }, "state":"firing", "activeAt":"2021-04-10T08:36:23.838961588Z", "value":"0e+00", "fingerprint": "" # 非 } """ self.item = item self.labels = self.item.get("labels", {}) self.annotations = self.item.get("annotations", {}) self.fingerprint = self.item.get("fingerprint", "") @staticmethod def _get(items, key): return items.get(key, "DEFAULT_DATA") # @property # def is_resolved(self): # return self.item.get("status") == 'resolved' # @property # def is_alert(self): # return self._get(self.labels, "severity") in ["critical", "warning"] def node_exporter(self): return dict( alert_type="host", alert_host_ip=self._get(self.labels, "instance"), alert_service_name="", alert_service_type="", alert_service_en_type="" ) def exporter(self): alert_host_ip = self._get(self.labels, "instance") alert_service = self._get(self.labels, "job") alert_service_name = alert_service.replace("Exporter", "").strip() alert_service_type, alert_service_en_type = get_service_type( alert_host_ip, alert_service_name) app_name_str = self._get(self.labels, "app") if self._get(self.labels, "app") else \ self._get(self.labels, "job").split("Exporter")[0] if not app_name_str: _alert_type = "service" component_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_COMPONENT).filter( service__is_base_env=False) if list(filter( lambda x: x.service.app_name == app_name_str, list( component_list))): _alert_type = "component" else: _alert_type = "service" return dict( alert_type=_alert_type, alert_host_ip=self._get(self.labels, "instance"), alert_service_name=alert_service_name, alert_service_type=alert_service_type, alert_service_en_type=alert_service_en_type ) # def other(self): # alert_host_ip = self._get(self.labels, "ip") # alert_service_name = self._get(self.labels, "app").strip() # alert_service_type, alert_service_en_type = get_service_type( # alert_host_ip, alert_service_name) # return dict( # alert_type="service", # alert_host_ip=alert_host_ip, # alert_service_name=alert_service_name, # alert_service_type=alert_service_type, # alert_service_en_type=alert_service_en_type # ) def get_alert_time(self): start_time = self.item.get("startsAt", "") if not start_time: start_time = self.item.get("activeAt", "") return utc_to_local(utc_time_str=start_time) def analysis_labels(self): old_alert_type = self._get(self.labels, "job") if old_alert_type == "nodeExporter": kwargs = self.node_exporter() elif "Exporter" in old_alert_type: kwargs = self.exporter() else: # kwargs = self.other() kwargs = self.exporter() # TODO 有other场景出现再换 kwargs["status"] = self.item.get("status", "firing") kwargs["alert_level"] = self._get(self.labels, "severity") kwargs["alertname"] = self._get(self.labels, "alertname") kwargs["fingerprint"] = self.fingerprint kwargs.update(alert_time=self.get_alert_time()) if kwargs["alert_type"] == "service" or kwargs["alert_type"] == "component": kwargs["monitor"] = get_monitor_url( [{ "ip": kwargs.get("alert_host_ip"), "type": "service", "instance_name": kwargs.get("alert_service_name", "") }] ) kwargs["monitor_log"] = get_log_url( [{ "ip": kwargs.get("alert_host_ip"), "type": "service", "instance_name": kwargs.get("alert_service_name") }] ) else: kwargs["monitor"] = get_monitor_url( [{ "ip": kwargs.get("alert_host_ip"), "type": "host", "instance_name": "node" }] ) kwargs["monitor_log"] = get_log_url( [{ "ip": kwargs.get("alert_host_ip"), "type": "host", "instance_name": "node" }] ) return kwargs def analysis_annotations(self): return dict( alert_receiver=self._get(self.annotations, "consignee"), # 服务down:主机 10.0.12.35 中的 zookeeper 已经down掉超过一分钟. # 服务exporter down: # 主机 10.0.12.35 中的 zookeeper_exporter 已经down掉超过一分钟. alert_describe=self._get(self.annotations, "description") ) def __call__(self, env_id=1, *args, **kwargs): """ :param env_id: :param args: :param kwargs: :return: { "alert_type": 告警类型 host or service, "alert_host_ip": 告警来自哪个主机, "alert_host_system": 告警来自主机的系统, "alert_service_name": 告警服务名称, "alert_service_type": 告警服务所属产品名称, "alert_service_en_type": 告警服务类型,self_dev component database, "alert_level": 告警级别, "alert_describe": 告警描述, "alert_receiver": 告警接收人, "alert_resolve": 告警解决方案, "alert_time": 告警发生时间, "monitor": 跳转grafana地址, "monitor_log": 日志跳转url(仅服务存在), "fingerprint": 告警对应的唯一标识, "status": 告警状态: resolved,恢复;firing:告警 # 推送使用,其他无用, "alertname": 告警指标: # 推送使用,其他无用, } """ # if not self.is_alert: # return {} alert_info = self.analysis_labels() if alert_info["alert_type"] == "host": host = Host.objects.filter( ip=alert_info["alert_host_ip"]).first() if not host: return {} alert_info["env_id"] = host.env_id alert_info["alert_instance_name"] = host.instance_name else: ser = Service.objects.filter( service__app_name=alert_info["alert_service_name"], ip=alert_info["alert_host_ip"] ).first() # host = Host.objects.filter( # ip=alert_info["alert_host_ip"]).first() if not ser: return {} alert_info["env_id"] = ser.env_id alert_info["alert_instance_name"] = ser.service_instance_name alert_info.update(**self.analysis_annotations()) # if env_id and int(env_id) != alert_info["env_id"]: # return {} # TODO 等待env开发完成 return alert_info ================================================ FILE: omp_server/promemonitor/alertmanager.py ================================================ import json import logging from datetime import datetime, timedelta from db_models.models import MonitorUrl from utils.parse_config import MONITOR_PORT, PROMETHEUS_AUTH from db_models.models import Maintain import pytz import requests logger = logging.getLogger('server') class Alertmanager: """ 定义alertmanager的参数及动作 """ def __init__(self): self.basic_url = self.get_alertmanager_config() self.basic_auth = (PROMETHEUS_AUTH.get("username", "omp"), PROMETHEUS_AUTH.get("plaintext_password", "")) self.headers = {'Content-Type': 'application/json'} self.add_url = f'http://{self.basic_url}/api/v1/silences' # NOQA self.delete_url = f'http://{self.basic_url}/api/v1/silence' # NOQA self.select_url = f'http://{self.basic_url}/api/v1/silence' # NOQA @staticmethod def get_alertmanager_config(): alertmanager_url_config = MonitorUrl.objects.filter( name='alertmanager').first() if not alertmanager_url_config: # 默认值 return f'127.0.0.1:{MONITOR_PORT.get("alertmanager", 19013)}' monitor_url = alertmanager_url_config.monitor_url if monitor_url: return monitor_url return f'127.0.0.1:{MONITOR_PORT.get("alertmanager", 19013)}' # 默认值 @staticmethod def format_time(_time): """ 时区转换 """ if not _time: return (datetime.now(tz=pytz.UTC)).strftime( "%Y-%m-%dT%H:%M:%SZ") if isinstance(_time, datetime): return _time.astimezone(tz=pytz.UTC).strftime( "%Y-%m-%dT%H:%M:%SZ") return None def add_setting(self, value, name="env", start_time=None, ends_time=None): """ 设置维护模式 :param value: 值 instance 对应ip, env对应env_name :param name: 值的key:instance, env :param start_time: startsAt:type: datetime :param ends_time: endsAt:type: datetime :return: 成功返回: "25b1ea3e-73db-43cd-ae81-a397f9e1bc88" (silenceId) 失败:None """ start_time_str = self.format_time(start_time) if not start_time_str: return None if not ends_time: ends_time = datetime.now() + timedelta(days=30) ends_time_str = self.format_time(ends_time) if not ends_time_str: return None data = { "matchers": [ {"name": name, "value": value} ], "startsAt": start_time_str, "endsAt": ends_time_str, "createdBy": "api", "comment": "Silence", "status": {"state": "active"} } try: logger.info(data) resp = requests.post( self.add_url, data=json.dumps(data), headers=self.headers, timeout=5, auth=self.basic_auth ).json() if resp.get("status") == "success": logger.info(resp) return resp.get("data").get("silenceId", None) except Exception as e: logger.error(str(e)) return None def delete_setting(self, silence_id): """ 删除告警屏蔽规则 :param silence_id: 规则id :return: 成功 True, 失败 False """ try: resp = requests.delete( f"{self.delete_url}/{silence_id}", timeout=5, auth=self.basic_auth).json() except Exception as e: logger.error(str(e)) return False logger.info(resp) if resp.get("status") != "success": logger.error(resp.get("error")) return True return False def set_maintain_by_host_list(self, host_list): """ 将单个/多个主机设置为维护状态 """ maintain_list = list() maintain_id_list = list() for item in host_list: maintain_id = self.add_setting( value=item.get('ip'), name='instance') if not maintain_id: logger.error(f'设置主机{item.get("ip")}维护失败!') return None maintain = Maintain(matcher_name='instance', matcher_value=item.get('ip'), maintain_id=maintain_id) maintain_list.append(maintain) maintain_id_list.append(maintain_id) Maintain.objects.bulk_create(maintain_list) return maintain_id_list def set_maintain_by_env_name(self, env_name): """ 将指定env的主机设置为维护状态 """ maintain_id = self.add_setting(value=env_name, name='env') if not maintain_id: return None Maintain.objects.create(matcher_name='env', matcher_value=env_name, maintain_id=maintain_id) return maintain_id def revoke_maintain_by_host_list(self, host_list): for item in host_list: maintain = Maintain.objects.filter( matcher_name='instance', matcher_value=item.get('ip')).first() if not maintain: return False maintain_id = maintain.maintain_id delete_setting_result = self.delete_setting(maintain_id) if not delete_setting_result: try: resp = requests.get( f"{self.select_url}/{maintain_id}", timeout=5, auth=self.basic_auth).json() if resp.get("status") == "success" and resp.get("data").get("status").get("state") == "expired": Maintain.objects.filter( maintain_id=maintain_id).delete() return True except Exception as e: logger.error(str(e)) return False Maintain.objects.filter(maintain_id=maintain_id).delete() return True def revoke_maintain_by_env_name(self, env_name): maintain = Maintain.objects.filter( matcher_name='env', matcher_value=env_name).first() if not maintain: return False maintain_id = maintain.maintain_id self.delete_setting(maintain_id) Maintain.objects.filter(maintain_id=maintain_id).delete() return True ================================================ FILE: omp_server/promemonitor/apps.py ================================================ from django.apps import AppConfig class PromemonitorConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'promemonitor' ================================================ FILE: omp_server/promemonitor/custom_script_serializers.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author:' Lingyang.guo' # CreateDate: 14:26 import json from rest_framework import serializers from rest_framework.serializers import ModelSerializer from db_models.models.custom_metric import CustomScript class CustomScriptSerializer(ModelSerializer): bound_hosts_num = serializers.SerializerMethodField() class Meta: model = CustomScript fields = "__all__" def get_bound_hosts_num(self, obj): # NOQA bound_hosts = obj.bound_hosts if isinstance(bound_hosts, str): bound_hosts_num = len(json.loads(bound_hosts)) else: bound_hosts_num = len(bound_hosts) return bound_hosts_num ================================================ FILE: omp_server/promemonitor/custom_script_views.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author:' Lingyang.guo' # CreateDate: 14:08 import json import logging import os.path import traceback import requests import yaml from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from rest_framework.mixins import ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin from rest_framework.serializers import Serializer from rest_framework.viewsets import GenericViewSet from rest_framework.response import Response # from db_models.models import UploadFileHistory from db_models.models.custom_metric import CustomScript from promemonitor.custom_script_serializers import CustomScriptSerializer from promemonitor.promemonitor_filters import CustomScriptFilter from utils.common.paginations import PageNumberPager from promemonitor.prometheus_utils import PrometheusUtils from utils.parse_config import MONITOR_PORT from promemonitor.prometheus_utils import CW_TOKEN logger = logging.getLogger('server') class CustomScriptViewSet(GenericViewSet, ListModelMixin, CreateModelMixin, UpdateModelMixin, DestroyModelMixin): """ list: 查询自定义脚本列表 create: 新增自定义脚本记录 update: 更新自定义脚本模型字段 delete: 删除指定自定义脚本 """ get_description = "读取自定义脚本记录" post_description = "更新自定义脚本记录" serializer_class = CustomScriptSerializer pagination_class = PageNumberPager # 过滤,排序字段 filter_backends = (DjangoFilterBackend, OrderingFilter) queryset = CustomScript.objects.all().order_by("-created") filter_class = CustomScriptFilter prometheus_util = PrometheusUtils() def list(self, request, *args, **kwargs): """ 获取自定义脚本列表信息 """ queryset = self.filter_queryset(self.get_queryset()) # 分页,过滤,排序 page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) return self.get_paginated_response(serializer.data) serializer = self.get_serializer(queryset, many=True) return Response(serializer.data) def create(self, request, *args, **kwargs): """ 新增一条自定义脚本记录 """ file_obj = request.FILES.get('file') script_name = file_obj.name if CustomScript.objects.filter(script_name=script_name).exists(): return Response(data={"code": 1, "message": "该脚本已存在,请重新上传!"}) scrape_interval = request.data.get("scrape_interval", 60) enabled = request.data.get("enabled", 1) script_content = file_obj.read() script_content = str(script_content, encoding="utf8") metrics = list() for line in script_content.split("\n"): if "def get_" in line and ":" in line: metric = line.split("def get_")[1].split("(")[0].strip() metrics.append(metric) metric_num = len(metrics) description = script_content.split("\n")[0] try: custom_script = CustomScript( script_name=script_name, script_content=script_content, metrics=metrics, metric_num=metric_num, scrape_interval=scrape_interval, enabled=enabled, description=description, bound_hosts=[] ) custom_script.save() # UploadFileHistory.location(file=file_obj, module_obj=custom_script, user=request.user) script_job_str = script_name.split('.', 1)[0] prom_job_dict = { "job_name": f"{script_job_str}Exporter", "metrics_path": f"/metrics/monitor/{script_job_str}", "file_sd_configs": [ { "refresh_interval": f"{scrape_interval}s", "files": [ f"targets/{script_job_str}Exporter_all.json" ] } ] } with open(self.prometheus_util.prometheus_conf_path, "r") as fr: content = yaml.load(fr.read(), yaml.Loader) content.get("scrape_configs").append(prom_job_dict) with open(self.prometheus_util.prometheus_conf_path, "w", encoding="utf8") as fw: yaml.dump(data=content, stream=fw, allow_unicode=True, sort_keys=False) job_target_json = os.path.join(self.prometheus_util.prometheus_targets_path, f"{script_job_str}Exporter_all.json") if not os.path.exists(job_target_json): with open(job_target_json, "w") as target_fw: json.dump([], target_fw) reload_prometheus_url = "http://localhost:19011/-/reload" requests.post(reload_prometheus_url, auth=self.prometheus_util.basic_auth) return Response({}) except Exception as e: logger.error(traceback.format_exc(e)) return Response(data={"code": 1, "message": "新增自定义脚本失败!"}) def update(self, request, *args, **kwargs): """ 只可以更改绑定主机列表,启用状态和探测周期 """ instance = self.get_object() new_scrape_interval = request.data.get("scrape_interval") new_enabled = request.data.get("enabled", 1) new_bound_host_list = request.data.get("bound_hosts") new_description = request.data.get("description", instance.description) add_host_list = set(new_bound_host_list) - set(instance.bound_hosts) deleted_host_list = set(instance.bound_hosts) - \ set(new_bound_host_list) instance.scrape_interval = new_scrape_interval instance.enabled = new_enabled instance.bound_hosts = new_bound_host_list instance.description = new_description instance.save() headers = {"Content-Type": "application/json"} headers.update(CW_TOKEN) try: script_job_str = instance.script_name.split('.', 1)[0] prom_target_list = list() monitor_agent_port = MONITOR_PORT.get('monitorAgent', 19031) for item in (add_host_list | deleted_host_list): if item in add_host_list: agent_custom_script_url = f"http://{item}:{monitor_agent_port}/update/custom_scripts/add" # NOQA elif item in deleted_host_list: agent_custom_script_url = f"http://{item}:{monitor_agent_port}/update/custom_scripts/delete" # NOQA else: continue payload = { "custom_scripts": [{ "script_name": script_job_str, "script_content": instance.script_content, "script_metrics": instance.metrics }] } payload = json.dumps(payload) res = requests.post( url=agent_custom_script_url, headers=headers, data=payload) if res.status_code != 200: logger.error(f"向主机{item}agent发送添加自定义脚本信息失败!") return Response(data={"code": 1, "message": f"向主机{item}agent发送添加自定义脚本信息失败!"}) for host in instance.bound_hosts: prom_target_list.append({ "targets": [ f"{host}:{monitor_agent_port}" ], "labels": { "instance": f"{host}", "service_type": "service", "env": "default" } }) job_target_json = os.path.join(self.prometheus_util.prometheus_targets_path, f"{script_job_str}Exporter_all.json") with open(job_target_json, "w") as jtj_fw: json.dump(prom_target_list, jtj_fw, indent=4) except Exception as e: logger.error(traceback.format_exc(e)) logger.error("向agent发送添加自定义脚本信息失败!") return Response(data={"code": 1, "message": "添加自定义脚本信息失败!"}) return Response() def destroy(self, request, *args, **kwargs): """ 向agent发送删除该自定义脚本信息;删除prometheus任务;删除库记录 """ instance = self.get_object() script_job_str = instance.script_name.split('.', 1)[0] with open(self.prometheus_util.prometheus_conf_path, "r") as fr: content = yaml.load(fr.read(), yaml.Loader) for i in content.get("scrape_configs"): if i.get("job_name") == f"{script_job_str}Exporter": content.get("scrape_configs").remove(i) with open(self.prometheus_util.prometheus_conf_path, "w", encoding="utf8") as fw: yaml.dump(data=content, stream=fw, allow_unicode=True, sort_keys=False) job_target_json = os.path.join(self.prometheus_util.prometheus_targets_path, f"{script_job_str}Exporter_all.json") if os.path.exists(job_target_json): os.remove(job_target_json) if len(instance.bound_hosts) == 0: instance.delete() return Response({}) monitor_agent_port = MONITOR_PORT.get('monitorAgent', 19031) headers = {"Content-Type": "application/json"} headers.update(CW_TOKEN) for host in instance.bound_hosts: agent_delete_custom_script_url = f"http://{host}:{monitor_agent_port}/update/custom_scripts/delete" # NOQA payload = { "custom_scripts": [{ "script_name": script_job_str }] } payload = json.dumps(payload) res = requests.post( url=agent_delete_custom_script_url, headers=headers, data=payload) if res.status_code != 200: logger.error(f"向主机{host}agent发送删除自定义脚本信息失败!") return Response(data={"code": 1, "message": f"向主机{host}agent发送删除自定义脚本信息失败!"}) instance.delete() return Response({}) class CustomScriptJobInfoView(GenericViewSet, ListModelMixin): get_description = "读取自定义脚本任务信息" # queryset = CustomScript.objects.all().order_by("-created") serializer_class = Serializer prometheus_util = PrometheusUtils() def list(self, request, *args, **kwargs): """ 读取自定义脚本任务信息 """ cs_id = request.query_params.get("id") instance = CustomScript.objects.get(id=cs_id) script_job_str = instance.script_name.split('.', 1)[0] job_str = f"{script_job_str}Exporter" prometheus_targets_url = f"http://127.0.0.1:{MONITOR_PORT.get('prometheus', '19011')}/api/v1/targets" try: res = requests.get(url=prometheus_targets_url, auth=self.prometheus_util.basic_auth) if res.status_code != 200: return Response(data={"code": 1, "message": "获取自定义脚本任务信息失败!"}) active_targets_list = res.json().get("data").get("activeTargets") custom_script_job_list = list() for active_target in active_targets_list: if active_target.get("scrapePool") == job_str: custom_script_job_info = { "scrape_url": active_target.get("scrapeUrl"), "status": active_target.get("health"), "last_scrape_duration": active_target.get("lastScrapeDuration"), "last_error": active_target.get("lastError") } custom_script_job_list.append(custom_script_job_info) return Response(custom_script_job_list) # return Response(data={"code": 1, "message": "获取自定义脚本任务信息失败!"}) except Exception as e: logger.error(traceback.format_exc(e)) return Response(data={"code": 1, "message": "获取自定义脚本任务信息失败!"}) ================================================ FILE: omp_server/promemonitor/grafana_url.py ================================================ from db_models.models import GrafanaMainPage, MonitorUrl, Host, ApplicationHub, Service import requests import json import logging import pytz import datetime import traceback from omp_server.settings import TIME_ZONE from utils.parse_config import PROMETHEUS_AUTH logger = logging.getLogger('server') class CurlPrometheus(object): @staticmethod def curl_prometheus(): """ 请求prometheus接口返回相应json """ prometheus_auth = (PROMETHEUS_AUTH.get("username"), PROMETHEUS_AUTH.get("plaintext_password")) monitor_ip = MonitorUrl.objects.filter(name="prometheus") monitor_url = monitor_ip[0].monitor_url if len( monitor_ip) else "127.0.0.1:19013" try: url = f"http://{monitor_url}/api/v1/alerts" response = requests.request( "GET", url, headers={}, data="", auth=prometheus_auth) return json.loads(response.text) except Exception as e: logger.error("prometheus请求alerts失败:" + str(e)) return {"status": "-1"} def utc_local(utc_time_str, utc_format='%Y-%m-%dT%H:%M:%SZ'): """ 时区转换,如果转换报错,那么使用当前时间作为返回值 :type utc_time_str str :param utc_time_str: utc时间字符串 :type utc_format str :param utc_format: utc时间格式 :return: """ try: utc_time_str = utc_time_str.split( ".")[0] + utc_time_str.split(".")[-1][-1] local_tz = pytz.timezone(TIME_ZONE) local_format = "%Y-%m-%d %H:%M:%S" utc_dt = datetime.datetime.strptime(utc_time_str, utc_format) local_dt = utc_dt.replace(tzinfo=pytz.utc).astimezone(local_tz) time_str = local_dt.strftime(local_format) return time_str except Exception as e: logger.error(f"在转化时间格式时报错: {str(e)}\n详情为: {traceback.format_exc()}") return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") def get_item_type(item, database_list, service_list, component_list): """ 获取item类型 """ app_name_str = "" if item.get("labels").get("job") == "nodeExporter": return "host" app_name_str = item.get("labels").get("app") if item.get("labels").get("app") else \ item.get("labels").get("job", "").split("Exporter")[0] if not app_name_str: return "service" if list(filter( lambda x: x.service.app_name == app_name_str, list(database_list))): return "database" elif list(filter( lambda x: x.service.app_name == app_name_str, list( service_list) )): return "service" elif list(filter( lambda x: x.service.app_name == app_name_str, list( component_list) )): return "component" else: return "service" def explain_prometheus(params): """ 生成前端异常清单所需要的json列表 """ ignore_status_list = [Service.SERVICE_STATUS_NORMAL, Service.SERVICE_STATUS_STARTING, Service.SERVICE_STATUS_STOPPING, Service.SERVICE_STATUS_RESTARTING, Service.SERVICE_STATUS_STOP] host_list = Host.objects.values('ip', 'instance_name') host_ip_list = [host.get("ip") for host in host_list] database_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_COMPONENT).filter( service__app_labels__label_name__contains="数据库").filter( service_status__in=ignore_status_list).filter( service__is_base_env=False) service_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_SERVICE).filter( service_status__in=ignore_status_list).filter( service__is_base_env=False).filter( service_controllers__start__isnull=False) component_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_COMPONENT).filter( service_status__in=ignore_status_list).filter( service__is_base_env=False) r = CurlPrometheus.curl_prometheus() if r.get('status') == 'success': prometheus_info = [] compare_list = [] alerts = r.get('data', {}).get('alerts') prometheus_alerts = sorted( alerts, key=lambda e: e.get('labels').__getitem__('severity'), reverse=False) for lab in prometheus_alerts: if lab.get('status') == 'resolved': continue label = lab.get('labels') if label.get("instance", "") not in host_ip_list: continue tmp_dict = {} tmp_list = [label.get('alertname'), label.get( 'instance_name'), label.get('job')] if tmp_list in compare_list: continue compare_list.append(tmp_list) _type = get_item_type(item=lab, database_list=database_list, service_list=service_list, component_list=component_list) tmp_dict['type'] = _type _ip = label.get('instance').split(":")[0] if label.get('instance') else '' tmp_dict['ip'] = _ip s_app = label.get('app') if label.get( 'app') else label.get("job", "").split("Exporter")[0] tmp_dict['instance_name'] = f"{s_app}-{_ip.split('.')[2]}-{_ip.split('.')[3]}" if _type != "host" else s_app tmp_dict['severity'] = label.get('severity') annotation = lab.get('annotations') tmp_dict['description'] = annotation.get('description') lab_date = lab.get('activeAt') if lab.get( 'activeAt') else lab.get('startsAt') tmp_dict['date'] = utc_local(lab_date) prometheus_info.append(tmp_dict) prometheus_json = explain_url(prometheus_info) if params: prometheus_json = explain_filter(prometheus_json, params) return prometheus_json else: logger.error("prometheus请求alerts失败:" + str(r)) return "error" def explain_filter(prometheus_json, params): """ 递归筛选 """ if not params: return prometheus_json fil_filed = params.popitem() fil_info = [] for j in prometheus_json: value = j.get(fil_filed[0]) if value and fil_filed[1].lower() in value.lower(): fil_info.append(j) # TODO 组件集合包含数据库 if fil_filed[1].lower() == "component" and value.lower() == "database": fil_info.append(j) return explain_filter(fil_info, params) def explain_url(explain_info, is_service=None, is_host=None): """ 封装dict添加grafana的url """ # monitor_ip = MonitorUrl.objects.filter(name="grafana") # grafana_ins = monitor_ip[0].monitor_url if len( # monitor_ip) else "127.0.0.1:19013" # grafana_url = f"http://{grafana_ins}" # 去掉跳转grafana中携带的ip、port grafana_url = "" url_dict = {} for i in GrafanaMainPage.objects.all(): url_dict[i.instance_name] = i.instance_url for instance_info in explain_info: # TODO 待确认 跳转服务面板使用 app_name 而不是 instance_name ? if is_service: service_name = instance_info.get('app_name') else: service_name = instance_info.get('instance_name', '').split("-")[0] if instance_info.get('is_web'): instance_info['monitor_url'] = None instance_info['log_url'] = None continue service_ip = instance_info.get('ip') if instance_info.get('type') != 'host' and not is_host \ or is_service: service_instance_name = instance_info.get('service_instance_name') monitor_url = url_dict.get(service_name.lower() if isinstance(service_name, str) else service_name) instance_info['cluster_url'] = "" if monitor_url: instance_info['monitor_url'] = grafana_url + \ monitor_url + f"?var-instance={service_ip}&kiosk=tv" if Service.objects.filter( service_instance_name=service_instance_name).first() and Service.objects.filter( service_instance_name=service_instance_name).first().cluster: cluster_name = Service.objects.filter( service_instance_name=service_instance_name).first().cluster.cluster_name cluster_monitor_url = url_dict.get(f"{service_name}cluster", "") instance_info[ 'cluster_url'] = cluster_monitor_url + f"?var-cluster={cluster_name}&kiosk=tv" if cluster_monitor_url else "" else: try: if service_name and ApplicationHub.objects.filter( app_name=service_name ).first() and ApplicationHub.objects.filter( app_name=service_name ).first().app_monitor and ApplicationHub.objects.filter( app_name=service_name ).first().app_monitor.get("type") == "JavaSpringBoot": instance_info['monitor_url'] = grafana_url + url_dict.get( 'javaspringboot', 'nojavaspringboot') + \ f"?var-env=default&var-ip={service_ip}" \ f"&var-app={service_name}&var-job={service_name}Exporter&kiosk=tv" else: instance_info['monitor_url'] = grafana_url + url_dict.get( 'service', 'noservice') + f"?var-ip={service_ip}&var-app={service_name}&kiosk=tv" except Exception as e: logger.error(e) instance_info['monitor_url'] = grafana_url + url_dict.get( 'service', 'noservice') + f"?var-ip={service_ip}&var-app={service_name}&kiosk=tv" instance_info['log_url'] = grafana_url + \ url_dict.get( 'log', 'nolog') + f"?var-env=default&var-app={service_name}" + f"&var-instance={service_instance_name}" else: instance_info['monitor_url'] = grafana_url + \ url_dict.get('node', 'nohosts') + \ f"?var-node={service_ip}&kiosk=tv" instance_info['log_url'] = None return explain_info ================================================ FILE: omp_server/promemonitor/grafana_views.py ================================================ # -*- coding: utf-8 -*- # Project: grafana_views # Author: jon.liu@yunzhihui.com # Create time: 2021-10-12 10:27 # IDE: PyCharm # Version: 1.0 # Introduction: """ 跳转grafana页面使用 """ import re import logging import requests import traceback from urllib.parse import urlparse from django.http import QueryDict # from django.http import HttpRequest from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from db_models.models import MonitorUrl logger = logging.getLogger("server") def grafana_proxy(request, url, requests_args=None): """ 跳转grafana使用的函数 :type request HttpRequest :param request: http请求对象 :type url str :param url: url :type requests_args dict :param requests_args: 请求参数 :return: """ requests_args = (requests_args or {}).copy() headers = {} for key, value in request.META.items(): if key.startswith('HTTP_') and key != 'HTTP_HOST': headers[key[5:].replace('_', '-')] = value elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): headers[key.replace('_', '-')] = value params = request.GET.copy() if 'headers' not in requests_args: requests_args['headers'] = {} if 'data' not in requests_args: requests_args['data'] = request.body if 'params' not in requests_args: requests_args['params'] = QueryDict('', mutable=True) headers.update(requests_args['headers']) params.update(requests_args['params']) for key in list(headers.keys()): if key.lower() == 'content-length': del headers[key] headers["X-CW-USER"] = "omp" requests_args['headers'] = headers requests_args['params'] = params response = requests.request( request.method, url, **requests_args) proxy_response = HttpResponse( response.content, status=response.status_code) excluded_headers = { 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade', 'content-encoding', 'content-length' } for key, value in response.headers.items(): if key.lower() in excluded_headers: continue elif key.lower() == 'location': absolute_pattern = re.compile(r'^[a-zA-Z]+://.*$') if absolute_pattern.match(value): proxy_response[key] = value parsed_url = urlparse(response.url) if value.startswith('//'): proxy_response[key] = parsed_url.scheme + ':' + value elif value.startswith('/'): proxy_response[key] = parsed_url.scheme + \ '://' + parsed_url.netloc + value else: proxy_response[key] = \ parsed_url.scheme + '://' + \ parsed_url.netloc + \ parsed_url.path.rsplit('/', 1)[0] + '/' + value else: proxy_response[key] = value return proxy_response @csrf_exempt def grafana_proxy_view(request, path): """ 获取grafana页面 :type request HttpRequest :param request: 请求对象 :type path str :param path: 请求路径 :return: """ grafana = "default" try: monitor_ip = MonitorUrl.objects.filter(name="grafana") grafana_ins = monitor_ip[0].monitor_url if len( monitor_ip) else "127.0.0.1:19014" grafana = f"http://{grafana_ins}" remote_url = f'{grafana}/' + path return grafana_proxy(request, remote_url) except Exception as e: logger.error( f"跳转grafana失败: {str(e)};\n详情为: {traceback.format_exc()}") return HttpResponse( content=f"请确认grafana配置[{grafana}]可用!", status=200) ================================================ FILE: omp_server/promemonitor/promemonitor_filters.py ================================================ """ 主机相关过滤器 """ import time import django_filters from django_filters.rest_framework import FilterSet from db_models.models import (Alert, CustomScript, AlertRule) from rest_framework.filters import BaseFilterBackend class AlertFilter(FilterSet): """ Alert过滤类 """ alert_host_ip = django_filters.CharFilter( help_text="ALERT_HOST_IP,模糊匹配", field_name="alert_host_ip", lookup_expr="icontains") alert_instance_name = django_filters.CharFilter( help_text="ALERT_INSTANCE_NAME,模糊匹配", field_name="alert_instance_name", lookup_expr="icontains") alert_level = django_filters.CharFilter( help_text="ALERT_LEVEL,模糊匹配", field_name="alert_level", lookup_expr="icontains") alert_type = django_filters.CharFilter( help_text="ALERT_TYPE,模糊匹配", field_name="alert_type", lookup_expr="icontains") class Meta: model = Alert fields = ( "alert_host_ip", "alert_instance_name", "alert_instance_name", "alert_level", "alert_type" ) class QuotaFilter(FilterSet): """ 指标规则过滤类 """ alert = django_filters.CharFilter( help_text="alert,规则名称模糊匹配", field_name="alert", lookup_expr="icontains" ) class Meta: model = AlertRule fields = ( "alert", ) class MyTimeFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): start_alert_time = request.GET.get('start_alert_time') end_alert_time = request.GET.get('end_alert_time') if (not start_alert_time) or (not end_alert_time): return queryset.all() try: time.strptime(start_alert_time, "%Y-%m-%d %H:%M:%S") time.strptime(end_alert_time, "%Y-%m-%d %H:%M:%S") except ValueError: return queryset.all() return queryset.filter(alert_time__range=(start_alert_time, end_alert_time)) class CustomScriptFilter(FilterSet): """ 自定义脚本过滤类 """ description = django_filters.CharFilter( help_text="自定义脚本描述,模糊匹配", field_name="description", lookup_expr="icontains") class Meta: model = CustomScript fields = ("description",) ================================================ FILE: omp_server/promemonitor/promemonitor_serializers.py ================================================ import datetime import logging from django.db.models import F from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.serializers import ModelSerializer, ListSerializer, \ Serializer from db_models.models import Host, MonitorUrl, Alert, Maintain, Service, Rule, AlertRule, WaitSelfHealing from promemonitor.alert_util import AlertAnalysis from promemonitor.alertmanager import Alertmanager from promemonitor.tasks import monitor_agent_restart from utils.common.exceptions import OperateError from utils.common.serializers import HostIdsSerializer from utils.common.validators import ( NoEmojiValidator, NoChineseValidator ) logger = logging.getLogger('server') class MonitorUrlListSerializer(ListSerializer): def update(self, instance, validated_data): pass def to_internal_value(self, data): return data.get('data') def validate(self, data): queryset = MonitorUrl.objects.all() method = self.context["request"].method for i in data: if method in ('PATCH', 'PUT', 'DELETE'): if not i.get("id"): raise serializers.ValidationError("id是必须字段") monitor_url = i.get("monitor_url") if method != 'GET': if not monitor_url: raise serializers.ValidationError("monitor_url是必须字段") if len(monitor_url) > 128: raise serializers.ValidationError( f"monitor_url字段超过128,detail:{monitor_url}") name = i.get("name") if name: if queryset.filter(name=name).exists(): raise serializers.ValidationError( f"name字段已经存在,detail:{name}") if len(name) > 32: raise serializers.ValidationError( f"name字段长度超过32,detail:{name}") else: raise serializers.ValidationError("name字段不为空") return data def create(self, validated_data): monitor = [MonitorUrl(**item) for item in validated_data] return MonitorUrl.objects.bulk_create(monitor) class MonitorUrlSerializer(ModelSerializer): """ 监控配置项序列化 """ name = serializers.CharField( max_length=32, required=True, error_messages={"invalid": "监控名字重复"}, help_text="监控名字") id = serializers.IntegerField( max_value=100, required=False, error_messages={"invalid": "id格式不正确"}, help_text="id") monitor_url = serializers.CharField( required=True, error_messages={"invalid": "监控地址格式不正确"}, validators=[ NoEmojiValidator(message="url地址存在非法字符"), NoChineseValidator(message="url地址存在非法字符"), ], help_text="监控地址") class Meta: model = MonitorUrl fields = "__all__" list_serializer_class = MonitorUrlListSerializer def validate_name(self, name): """ 校验name是否重复 """ queryset = MonitorUrl.objects.all() if self.instance is not None: queryset = queryset.exclude(id=self.instance.id) if queryset.filter(name=name).exists(): raise ValidationError("name已经存在") return name def validate_monitor_url(self, monitor_url): if len(monitor_url.split(" ")) > 1: raise ValidationError("url地址存在非法字符") return monitor_url class ListAlertSerializer(ModelSerializer): class Meta: model = Alert fields = "__all__" class UpdateAlertSerializer(Serializer): """ 告警记录序列化 """ ids = serializers.ListField( help_text="告警记录ID列表", required=True, error_messages={"required": "必须包含ID列表字段"} ) is_read = serializers.IntegerField( help_text="是否已读", required=True, error_messages={"required": "必须包含是否已读字段"} ) # def validate(self, attrs): # # TODO 做校验 # return attrs def create(self, validated_data): Alert.objects.filter(id__in=validated_data.get('ids')).update( is_read=validated_data.get('is_read')) return validated_data def update(self, instance, validated_data): pass class MaintainSerializer(ModelSerializer): maintain_id = serializers.CharField( help_text="维护唯一标识", required=False, max_length=1024, error_messages={"required": "maintain_id不可重复"} ) matcher_name = serializers.CharField( help_text="匹配标签", required=True, max_length=1024, error_messages={"required": "必须包含匹配标签"} ) matcher_value = serializers.CharField( help_text="维护唯一标识", required=True, max_length=1024, error_messages={"required": "必须包含匹配标签值"} ) def validate(self, attrs): """ 校验env是否存在 """ return attrs def create(self, validated_data): """ 进入 / 退出维护模式 """ matcher_name = validated_data.get("matcher_name") matcher_value = validated_data.get("matcher_value") maintain_queryset = Maintain.objects.filter( matcher_name=matcher_name, matcher_value=matcher_value) status = "开启" if not maintain_queryset.exists() else "关闭" # 根据 maintain_id 判断主机进入 / 退出维护模式 alert_manager = Alertmanager() if not maintain_queryset.exists(): res_ls = alert_manager.set_maintain_by_env_name(matcher_value) else: res_ls = alert_manager.revoke_maintain_by_env_name(matcher_value) # 操作失败 if not res_ls: logger.error(f"全局{status}维护模式失败") # 操作失败记录写入 raise OperateError(f"全局{status}维护模式失败") # 操作成功 logger.info(f"全局{status}维护模式成功") return validated_data class Meta: model = Maintain fields = "__all__" class ReceiveAlertSerializer(Serializer): receiver = serializers.CharField( help_text="接收者", required=True, error_messages={"required": "不可为空"}, ) status = serializers.CharField( help_text="状态", required=True, error_messages={"required": "不可为空"}, ) alerts = serializers.ListField( help_text="告警内容", required=True, error_messages={"required": "不可为空"}, ) def update(self, instance, validated_data): pass def create(self, validated_data): alerts = validated_data.get('alerts') for ele in alerts: alert_analysis = AlertAnalysis(ele) alert_info = alert_analysis() if not alert_info: continue if alert_info.get('status') != 'firing': continue alert = Alert( is_read=0, alert_type=alert_info.get('alert_type'), alert_host_ip=alert_info.get('alert_host_ip'), alert_service_name=alert_info.get('alert_service_name'), alert_instance_name=alert_info.get('alert_instance_name'), alert_service_type='', # TODO 暂时拿不到值 alert_level=alert_info.get('alert_level'), alert_describe=alert_info.get('alert_describe'), alert_receiver=alert_info.get('alert_receiver'), alert_resolve='', # TODO 待后续 alert_time=alert_info.get('alert_time'), create_time=datetime.datetime.now().strftime( "%Y-%m-%d %H:%M:%S"), monitor_path=alert_info.get('monitor'), monitor_log=alert_info.get('monitor_log'), fingerprint=alert_info.get('fingerprint'), # env='default' # TODO 此版本默认不赋值 ) alert.save() # TODO service_name暂时为告警类型 service_name = alert_info.get('alert_type') WaitSelfHealing.objects.create(repair_ser=alert_info, service_name=service_name) if alert_info.get('alert_type') == 'host': Host.objects.filter(ip=alert_info.get('alert_host_ip')).update( alert_num=F("alert_num") + 1) if alert_info.get('alert_type') == 'service': Service.objects.filter(service_instance_name=alert_info.get( 'alert_instance_name')).filter( ip=alert_info.get('alert_host_ip')).update( service_status=Service.SERVICE_STATUS_STOP, alert_count=F("alert_count") + 1) # TODO 后续在模型中增加异常字段 return validated_data class MonitorAgentRestartSerializer(HostIdsSerializer): """ 监控Agent重启序列化类 """ def update(self, instance, validated_data): pass def create(self, validated_data): """ 监控Agent重启 """ host_ids = validated_data.get("host_ids", []) filter_host_ids = list( Host.objects.filter( id__in=host_ids, monitor_agent__in=[ str(Host.AGENT_RUNNING), str(Host.AGENT_RESTART), str(Host.AGENT_START_ERROR) ] ).values_list("id", flat=True) ) for item in filter_host_ids: monitor_agent_restart.delay(item) # 下发任务后批量更新重启主机状态 Host.objects.filter( id__in=filter_host_ids ).update(monitor_agent=Host.AGENT_RESTART) return validated_data class RuleSerializer(ModelSerializer): """ 内置规则序列化类 """ class Meta: """ 元数据 """ model = Rule fields = '__all__' class QuotaSerializer(ModelSerializer): """ 告警规则序列化类 """ class Meta: """ 元数据 """ model = AlertRule fields = '__all__' ================================================ FILE: omp_server/promemonitor/prometheus.py ================================================ import json import logging import math import requests from db_models.models import MonitorUrl from utils.parse_config import MONITOR_PORT, PROMETHEUS_AUTH logger = logging.getLogger('server') class Prometheus: """ 定义prometheus的一些参数以及动作 """ STATUS = ("normal", "warning", "critical") def __init__(self): self.basic_url = self.get_prometheus_config() self.prometheus_api_query_url = f'http://{self.basic_url}/api/v1/query?query=' # NOQA self.basic_auth = (PROMETHEUS_AUTH.get( "username", "omp"), PROMETHEUS_AUTH.get("plaintext_password", "")) self.headers = {'Content-Type': 'application/json'} @staticmethod def get_prometheus_config(): prometheus_url_config = MonitorUrl.objects.filter( name='prometheus').first() if not prometheus_url_config: return f'127.0.0.1:{MONITOR_PORT.get("prometheus", 19011)}' # 默认值 monitor_url = prometheus_url_config.monitor_url if monitor_url: return monitor_url return f'127.0.0.1:{MONITOR_PORT.get("prometheus", 19011)}' # 默认值 @staticmethod def get_host_threshold(env_id=1,**kwargs): host_threshold = { 'cpu': (80, 90), 'mem': (80, 90), 'root_disk': (80, 90), 'data_disk': (80, 90), } try: from db_models.models import AlertRule cpu_warning_ht = AlertRule.objects.filter(env_id=env_id, name="CPU使用率", severity="warning").first() cpu_critical_ht = AlertRule.objects.filter(env_id=env_id, name="CPU使用率", severity="critical").first() mem_warning_ht = AlertRule.objects.filter(env_id=env_id, name="内存使用率", severity="warning").first() mem_critical_ht = AlertRule.objects.filter(env_id=env_id, name="内存使用率", severity="critical").first() root_disk_warning_ht = AlertRule.objects.filter(env_id=env_id, name="根分区使用率", severity="warning").first() root_disk_critical_ht = AlertRule.objects.filter(env_id=env_id, name="根分区使用率", severity="critical").first() data_disk_warning_ht = None data_disk_critical_ht = None if kwargs.get("data_dir"): """ 从指标规则中获取指定路径的数据分区 """ data_disk_warning_ht = AlertRule.objects.filter(env_id=env_id, name="数据分区使用率", severity="warning").first() data_disk_critical_ht = AlertRule.objects.filter(env_id=env_id, name="数据分区使用率", severity="critical").first() # # from db_models.models import HostThreshold # cpu_warning_ht = HostThreshold.objects.filter(env_id=env_id, index_type="cpu_used", # alert_level="warning").first() # cpu_critical_ht = HostThreshold.objects.filter(env_id=env_id, index_type="cpu_used", # alert_level="critical").first() # mem_warning_ht = HostThreshold.objects.filter(env_id=env_id, index_type="memory_used", # alert_level="warning").first() # mem_critical_ht = HostThreshold.objects.filter(env_id=env_id, index_type="memory_used", # alert_level="critical").first() # root_disk_warning_ht = HostThreshold.objects.filter(env_id=env_id, index_type="disk_root_used", # alert_level="warning").first() # root_disk_critical_ht = HostThreshold.objects.filter(env_id=env_id, index_type="disk_root_used", # alert_level="critical").first() # data_disk_warning_ht = HostThreshold.objects.filter(env_id=env_id, index_type="disk_data_used", # alert_level="warning").first() # data_disk_critical_ht = HostThreshold.objects.filter(env_id=env_id, index_type="disk_data_used", # alert_level="critical").first() host_threshold.update( cpu=( int(cpu_warning_ht.threshold_value) if cpu_warning_ht else 0, int(cpu_critical_ht.threshold_value) if cpu_critical_ht else 100, ), mem=( int(mem_warning_ht.threshold_value) if mem_warning_ht else 0, int(mem_critical_ht.threshold_value) if mem_critical_ht else 100, ), root_disk=( int(root_disk_warning_ht.threshold_value) if root_disk_warning_ht else 0, int(root_disk_critical_ht.threshold_value) if root_disk_critical_ht else 100, ), data_disk=( int(data_disk_warning_ht.threshold_value) if data_disk_warning_ht else 0, int(data_disk_critical_ht.threshold_value) if data_disk_critical_ht else 100, ) ) except Exception as e: logger.error(f"获取主机阈值失败,详情为:{e}") return host_threshold def get_host_metric_status(self, metric, metric_value, **kwargs): if metric_value is None: return None host_threshold = self.get_host_threshold(**kwargs) if metric_value > max(host_threshold.get(metric)): status = 'critical' elif metric_value < min(host_threshold.get(metric)): status = 'normal' else: status = 'warning' return status def get_host_cpu_usage(self, host_list): """ 获取指定主机cpu使用率 """ query_url = f'{self.prometheus_api_query_url}(1 - avg(rate(node_cpu_seconds_total' \ f'{{mode="idle"}}[2m])) by (instance))*100' # print(query_url) try: get_cpu_response = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) if get_cpu_response.status_code == 200: cpu_usage_dict = get_cpu_response.json() if cpu_usage_dict.get('status') != 'success': logger.error(get_cpu_response.text) logger.error('获取主机CPU使用率失败!') return host_list for index, host in enumerate(host_list.copy()): for item in cpu_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == host.get('ip'): host_list[index]['cpu_usage'] = math.ceil( float(item.get('value')[1])) host_list[index]['cpu_status'] = self.get_host_metric_status('cpu', math.ceil( float(item.get('value')[1]))) break host_list[index]['cpu_usage'] = None host_list[index]['cpu_status'] = None # TODO 待阈值判断 return host_list else: logger.error(get_cpu_response.text) logger.error('获取主机CPU使用率失败!') return host_list except requests.ConnectionError as e: logger.error(e) logger.error('获取主机CPU使用率失败!') return host_list except Exception as e: logger.error(e) logger.error('获取主机CPU使用率失败!') return host_list def get_host_mem_usage(self, host_list): """ 获取指定主机内存使用率 """ query_url = f'{self.prometheus_api_query_url}(1 - (node_memory_MemAvailable_bytes / ' \ f'(node_memory_MemTotal_bytes)))* 100' # print(query_url) try: get_mem_response = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) if get_mem_response.status_code == 200: mem_usage_dict = get_mem_response.json() if mem_usage_dict.get('status') != 'success': logger.error(get_mem_response.text) logger.error('获取主机内存使用率失败!') return host_list for index, host in enumerate(host_list.copy()): for item in mem_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == host.get('ip'): host_list[index]['mem_usage'] = math.ceil( float(item.get('value')[1])) host_list[index]['mem_status'] = self.get_host_metric_status('mem', math.ceil( float(item.get('value')[1]))) break host_list[index]['mem_usage'] = None host_list[index]['mem_status'] = None # TODO 待阈值判断 return host_list else: logger.error(get_mem_response.text) logger.error('获取主机内存使用率失败!') return host_list except requests.ConnectionError as e: logger.error(e) logger.error('获取主机内存使用率失败!') return host_list except Exception as e: logger.error(e) logger.error('获取主机内存使用率失败!') return host_list def get_host_root_disk_usage(self, host_list): """ 获取指定主机磁盘根分区使用率 """ query_url = f'{self.prometheus_api_query_url}(node_filesystem_size_bytes{{mountpoint="/"}} - ' \ f'node_filesystem_free_bytes{{mountpoint="/",fstype!="rootfs"}}) / ' \ f'(node_filesystem_avail_bytes{{mountpoint="/"}}-node_filesystem_free_bytes{{mountpoint="/"}} - ' \ f'(-node_filesystem_size_bytes{{mountpoint="/"}}))*100' # print(query_url) try: get_root_disk_response = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) if get_root_disk_response.status_code == 200: root_disk_usage_dict = get_root_disk_response.json() if root_disk_usage_dict.get('status') != 'success': logger.error(get_root_disk_response.text) logger.error('获取主机磁盘根分区使用率失败!') return host_list for index, host in enumerate(host_list.copy()): for item in root_disk_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == host.get('ip'): host_list[index]['root_disk_usage'] = math.ceil( float(item.get('value')[1])) host_list[index]['root_disk_status'] = self.get_host_metric_status('root_disk', math.ceil( float(item.get('value')[1]))) break host_list[index]['root_disk_usage'] = None host_list[index]['root_disk_status'] = None # TODO return host_list else: logger.error(get_root_disk_response.text) logger.error('获取主机磁盘根分区使用率失败!') return host_list except requests.ConnectionError as e: logger.error(e) logger.error('获取主机磁盘根分区使用率失败!') return host_list except Exception as e: logger.error(e) logger.error('获取主机磁盘根分区使用率失败!') return host_list def get_host_data_disk_usage(self, host_list): """ 获取指定主机磁盘数据分区使用率 """ for index, host in enumerate(host_list.copy()): host_ip = host.get('ip') host_data_disk = host.get('data_folder') query_url = f'{self.prometheus_api_query_url}' \ f'(node_filesystem_size_bytes{{mountpoint="{host_data_disk}",instance="{host_ip}"}} - ' \ f'node_filesystem_free_bytes{{mountpoint="{host_data_disk}",instance="{host_ip}"}}) / ' \ f'(node_filesystem_avail_bytes{{mountpoint="{host_data_disk}",instance="{host_ip}"}} - ' \ f'node_filesystem_free_bytes{{mountpoint="{host_data_disk}",instance="{host_ip}"}} - ' \ f'(-node_filesystem_size_bytes{{mountpoint="{host_data_disk}",instance="{host_ip}"}}))*100' try: get_data_disk_response = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) if get_data_disk_response.status_code == 200: data_disk_usage_dict = get_data_disk_response.json() if data_disk_usage_dict.get('status') != 'success': logger.error(get_data_disk_response.text) logger.error('获取主机磁盘数据分区使用率失败!') continue if not data_disk_usage_dict.get('data').get('result'): host_list[index]['data_disk_usage'] = None host_list[index]['data_disk_status'] = None for item in data_disk_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == host.get('ip'): host_list[index]['data_disk_usage'] = math.ceil( float(item.get('value')[1])) host_list[index]['data_disk_status'] = self.get_host_metric_status('data_disk', math.ceil( float(item.get('value')[1])),data_dir=host_data_disk) break host_list[index]['data_disk_usage'] = None host_list[index]['data_disk_status'] = None else: logger.error(get_data_disk_response.text) logger.error('获取主机磁盘数据分区使用率失败!') continue except requests.ConnectionError as e: logger.error(e) logger.error('获取主机磁盘数据分区使用率失败!') continue except Exception as e: logger.error(e) logger.error('获取主机磁盘数据分区使用率失败!') continue return host_list def get_host_info(self, host_list): """ 获取主机负载基本信息 """ for index, host in enumerate(host_list.copy()): host_list[index]['cpu_usage'] = None host_list[index]['cpu_status'] = None host_list[index]['mem_usage'] = None host_list[index]['mem_status'] = None host_list[index]['root_disk_usage'] = None host_list[index]['root_disk_status'] = None host_list[index]['data_disk_usage'] = None host_list[index]['data_disk_status'] = None host_list = self.get_host_cpu_usage(host_list) host_list = self.get_host_mem_usage(host_list) host_list = self.get_host_root_disk_usage(host_list) host_list = self.get_host_data_disk_usage(host_list) return host_list def get_all_service_status(self): """ 获取服务状态 0-运行; 1-停止 :return: """ query_url = f'{self.prometheus_api_query_url}probe_success' try: res_body = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) res_dic = json.loads(res_body.text) if res_dic.get("status") != "success": return False, {} service_data = res_dic.get("data", {}).get("result", []) service_status_dic = dict() for item in service_data: metric = item.get("metric", {}) if metric.get("service_type") != "service": continue _key = metric.get("instance", "") + "_" + \ metric.get("instance_name", "") _value = True if int( item.get("value", [0, 0])[-1]) == 1 else False service_status_dic[_key] = _value return True, service_status_dic except Exception as e: logger.error(f"从prometheus获取数据失败: {str(e)}") return False, {} def get_all_host_targets(self): query_url = f'{self.basic_url}/api/v1/targets' host_targets = list() try: res_body = requests.get(url=f"http://{query_url}", headers=self.headers, auth=self.basic_auth) # NOQA res_dic = json.loads(res_body.text) if res_dic.get("status") != "success": return False, {} targets_data = res_dic.get("data", {}).get("activeTargets") for item in targets_data: if item.get("labels", {}).get("job") != "nodeExporter": continue host_targets.append(item.get("labels").get("instance")) return True, host_targets except Exception as e: logger.error(f"从prometheus获取主机targets失败: {str(e)}") return False, [] def get_all_service_targets(self): query_url = f'{self.basic_url}/api/v1/targets' service_targets = list() try: res_body = requests.get(url=f"http://{query_url}", headers=self.headers, auth=self.basic_auth) # NOQA res_dic = json.loads(res_body.text) if res_dic.get("status") != "success": return False, {} targets_data = res_dic.get("data", {}).get("activeTargets") for item in targets_data: if item.get("labels", {}).get("service_type") != "service": continue service_targets.append( f'{item.get("labels").get("instance")}_{item.get("labels").get("instance_name")}') return True, service_targets except Exception as e: logger.error(f"从prometheus获取服务targets失败: {str(e)}") return False, [] @staticmethod def get_service_threshold(env_id=1): service_threshold = { 'cpu': (80, 90), 'mem': (80, 90) } # try: # from db_models.models import ServiceThreshold # cpu_warning_st = ServiceThreshold.objects.filter(env_id=env_id, index_type="service_cpu_used", # alert_level="warning").first() # cpu_critical_st = ServiceThreshold.objects.filter(env_id=env_id, index_type="service_cpu_used", # alert_level="critical").first() # mem_warning_st = ServiceThreshold.objects.filter(env_id=env_id, index_type="service_memory_used", # alert_level="warning").first() # mem_critical_st = ServiceThreshold.objects.filter(env_id=env_id, index_type="service_memory_used", # alert_level="critical").first() # service_threshold.update( # cpu=( # int(cpu_warning_st.condition_value) if cpu_warning_st else 0, # int(cpu_critical_st.condition_value) if cpu_critical_st else 100, # ), # mem=( # int(mem_warning_st.condition_value) if mem_warning_st else 0, # int(mem_critical_st.condition_value) if mem_critical_st else 100, # ) # ) # except Exception as e: # logger.error(f"获取服务阈值失败,详情为:{e}") return service_threshold def get_service_metric_status(self, metric, metric_value): if metric_value is None: return None service_threshold = self.get_service_threshold() if metric_value > max(service_threshold.get(metric)): status = 'critical' elif metric_value < min(service_threshold.get(metric)): status = 'normal' else: status = 'warning' return status def get_service_cpu_usage(self, service_list): """ 获取服务cpu使用率 """ open_source_query_url = f'{self.prometheus_api_query_url}service_process_cpu_percent' self_service_query_url = f'{self.prometheus_api_query_url}process_cpu_usage * 100' try: os_cpu_response = requests.get( url=open_source_query_url, headers=self.headers, auth=self.basic_auth) if os_cpu_response.status_code == 200: os_cpu_usage_dict = os_cpu_response.json() if os_cpu_usage_dict.get('status') != 'success': logger.error(os_cpu_response.text) logger.error('获取开源服务CPU使用率失败!') return service_list for index, os_service in enumerate(service_list.copy()): if os_service.get('app_name') == 'hadoop': os_service['app_name'] = os_service.get( 'service_instance_name', 'hadoop').split('_')[0] for item in os_cpu_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == os_service.get('ip') \ and item.get('metric').get('app') == os_service.get('app_name') \ and item.get('metric').get('env') == os_service.get('env'): service_list[index]['cpu_usage'] = math.ceil( float(item.get('value')[1])) service_list[index]['cpu_status'] = self.get_service_metric_status('cpu', math.ceil( float(item.get('value')[1]))) service_list[index]['has_cpu_value'] = 1 break service_list[index]['cpu_usage'] = None service_list[index]['cpu_status'] = None # TODO 待阈值判断 else: logger.error(os_cpu_response.text) logger.error('获取开源服务CPU使用率失败!') ss_cpu_response = requests.get( url=self_service_query_url, headers=self.headers, auth=self.basic_auth) if ss_cpu_response.status_code == 200: ss_cpu_usage_dict = ss_cpu_response.json() if ss_cpu_usage_dict.get('status') != 'success': logger.error(ss_cpu_response.text) logger.error('获取自研服务CPU使用率失败!') return service_list for index, ss_service in enumerate(service_list.copy()): if ss_service.get('has_cpu_value', 0) == 1: continue for item in ss_cpu_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == ss_service.get('ip') \ and item.get('metric').get('job') == f"{ss_service.get('app_name')}Exporter" \ and item.get('metric').get('env') == ss_service.get('env'): service_list[index]['cpu_usage'] = math.ceil( float(item.get('value')[1])) service_list[index]['cpu_status'] = self.get_service_metric_status('cpu', math.ceil( float(item.get('value')[1]))) break service_list[index]['cpu_usage'] = None service_list[index]['cpu_status'] = None # TODO 待阈值判断 else: logger.error(os_cpu_response.text) logger.error('获取自研服务CPU使用率失败!') return service_list except Exception as e: logger.error(f'获取服务cpu使用率失败,报错信息为:{e}') return service_list def get_service_mem_usage(self, service_list): open_source_query_url = f'{self.prometheus_api_query_url}service_process_memory_percent' jvm_total_bytes_query_url = f'{self.prometheus_api_query_url}sum(jvm_memory_max_bytes{{area="heap"}}) by (instance,job,application,env)' node_total_bytes_query_url = f'{self.prometheus_api_query_url}node_memory_MemTotal_bytes' try: os_mem_response = requests.get( url=open_source_query_url, headers=self.headers, auth=self.basic_auth) if os_mem_response.status_code == 200: os_mem_usage_dict = os_mem_response.json() if os_mem_usage_dict.get('status') != 'success': logger.error(os_mem_response.text) logger.error('获取开源服务内存使用率失败!') return service_list for index, os_service in enumerate(service_list.copy()): service_list[index]['mem_usage'] = None service_list[index]['mem_status'] = None for item in os_mem_usage_dict.get('data').get('result'): if item.get('metric').get('instance') == os_service.get('ip') \ and item.get('metric').get('app') == os_service.get('app_name') \ and item.get('metric').get('env') == os_service.get('env'): service_list[index]['mem_usage'] = math.ceil( float(item.get('value')[1])) service_list[index]['mem_status'] = self.get_service_metric_status('mem', math.ceil( float(item.get('value')[1]))) service_list[index]['has_mem_value'] = 1 break # service_list[index]['mem_usage'] = None # service_list[index]['mem_status'] = None else: logger.error(os_mem_response.text) logger.error('获取开源服务内存使用率失败!') jtb_response = requests.get( url=jvm_total_bytes_query_url, headers=self.headers, auth=self.basic_auth) if jtb_response.status_code == 200: jtb_dict = jtb_response.json() if jtb_dict.get('status') != 'success': logger.error(jtb_response.text) logger.error('获取自研服务内存使用量失败!') return service_list else: logger.error(os_mem_response.text) logger.error('获取自研服务内存使用率失败!') return service_list ntb_response = requests.get( url=node_total_bytes_query_url, headers=self.headers, auth=self.basic_auth) if ntb_response.status_code == 200: ntb_dict = ntb_response.json() if ntb_dict.get('status') != 'success': logger.error(ntb_response.text) logger.error('获取自研服务内存使用量失败!') return service_list else: logger.error(os_mem_response.text) logger.error('获取主机内存资源量失败!') return service_list for index, ss_service in enumerate(service_list.copy()): if ss_service.get('has_mem_value', 0) == 1: continue for item in jtb_dict.get('data').get('result'): for ele in ntb_dict.get('data').get('result'): if item.get('metric').get('instance') == ss_service.get('ip') \ and item.get('metric').get('instance') == ele.get('metric').get('instance') \ and item.get('metric').get('job') == f"{ss_service.get('app_name')}Exporter" \ and item.get('metric').get('env') == ss_service.get('env'): service_list[index]['mem_usage'] = math.ceil( float(item.get('value')[1]) / float(ele.get('value')[1]) * 100) service_list[index]['mem_status'] = self.get_service_metric_status('mem', service_list[index][ 'mem_usage']) break return service_list except Exception as e: logger.error(f'获取服务mem使用率失败,报错信息为:{e}') return service_list def get_service_info(self, service_list): """ 获取服务负载基本信息 """ for index, service in enumerate(service_list.copy()): service_list[index]['cpu_usage'] = None service_list[index]['cpu_status'] = None service_list[index]['mem_usage'] = None service_list[index]['mem_status'] = None service_list = self.get_service_cpu_usage(service_list) service_list = self.get_service_mem_usage(service_list) return service_list def get_quota_res(self,quota): """ 获取指标结果 """ query_url = f"{self.prometheus_api_query_url}{quota}" try: response = requests.get( url=query_url, headers=self.headers, auth=self.basic_auth) if response.status_code != 200: logger.error(f"测试promsql错误: 状态码为{response.status_code} 错误{response.text}") return False, response.text print(query_url,response.json()) if response.json().get("status") == "success": return True, response.json()["data"]["result"] return False, response.text except Exception as e: logger.error(f"测试promsql错误:{e}") return False, "访问prometheus错误" class UpdatePrometheusRule(object): """ 更新prometheus的配置文件 """ ================================================ FILE: omp_server/promemonitor/prometheus_utils.py ================================================ # -*- coding: utf-8 -*- # Project: prometheus_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-11 14:31 # IDE: PyCharm # Version: 1.0 # Introduction: """ prometheus更新监控项使用的工具集 """ import os import json import pickle import shutil import logging import requests import hashlib # import requests import yaml from ruamel.yaml import YAML from db_models.models import HostThreshold, ServiceCustomThreshold, AlertRule from omp_server.settings import PROJECT_DIR from utils.parse_config import MONITOR_PORT, PROMETHEUS_AUTH, LOKI_CONFIG # from utils.parse_config import MONITOR_PORT logger = logging.getLogger("server") CW_TOKEN = { 'CWAccessToken': 'FnQGiEXrYr6n8diKuY6cc61Zw3MMyLW9icwiUlHjyoAkBsBKCDIqmDZbf'} AGREE = "http" # 内置exporter列表 EXPORTERS = [ "beanstalk", "clickhouse", "elasticsearch", "httpd", "kafka", "mysql", "node", "postgreSql", "redis", "tengine", "zookeeper", "rocketmq", ] METRICS = { "arangodb": "_admin/metrics", "nacos": "nacos/actuator/prometheus", } class PrometheusUtils(object): """ prometheus工具集 """ def __init__(self): self.prometheus_conf_path = os.path.join( PROJECT_DIR, "component/prometheus/conf/prometheus.yml") self.prometheus_rules_path = os.path.join( PROJECT_DIR, "component/prometheus/conf/rules") self.prometheus_targets_path = os.path.join( PROJECT_DIR, "component/prometheus/conf/targets") self.prometheus_node_rule_tpl = os.path.join( PROJECT_DIR, "package_hub/prometheus_rules_template/node_rule.yml") self.prometheus_node_data_rule_tpl = os.path.join( PROJECT_DIR, "package_hub/prometheus_rules_template/node_data_rule.yml") self.prometheus_service_status_rule_tpl = os.path.join( PROJECT_DIR, "package_hub/prometheus_rules_template/service_status_rule.yml") self.prometheus_exporter_status_rule_tpl = os.path.join( PROJECT_DIR, "package_hub/prometheus_rules_template/exporter_status_rule.yml") # 邮件地址 self.email_address = "omp@cloudwise.com" self.monitor_port = 19031 self.node_exporter_targets_file = os.path.join( self.prometheus_targets_path, "nodeExporter_all.json" ) self.agent_request_header = {} self.basic_auth = (PROMETHEUS_AUTH.get( "username", "omp"), PROMETHEUS_AUTH.get("plaintext_password", "")) @staticmethod def replace_placeholder(path, placeholder_list): """ 替换文件中的占位符 :param path: 要替换的文件路径 :param placeholder_list: 占位符字典列表 [{"key":"value"}] :return: """ if not os.path.isfile(path): return False, f"{path} not exists!" with open(path, "r") as f: data = f.read() for item in placeholder_list: for k, v in item.items(): placeholder = "${{{}}}".format(k) data = data.replace(placeholder, str(v)) with open(path, "w") as f: f.write(data) @staticmethod def json_distinct(iterable_lst): """ json元素去重 :param iterable_lst: 去重对象 :return: 去重后的对象 """ res = [] for instance in iterable_lst: res.append(pickle.dumps(instance)) res = set(res) return [pickle.loads(i) for i in res] @staticmethod def get_dic_from_yaml(file_path): """ 从yaml中获取字典 :param file_path: 文件路径 :return: """ with open(file_path, "r", encoding="utf8") as fp: content = fp.read() my_yaml = YAML() return my_yaml.load(content) @staticmethod def write_dic_to_yaml(dic, file_path): """ 将字典写入yaml :param dic: 字典数据 :param file_path: yaml文件路径 :return: """ my_yaml = YAML() with open(file_path, "w", encoding="utf8") as fp: my_yaml.dump(dic, fp) @staticmethod def get_expr(num, env, data_path): """ 获取prometheus数据分区匹配规则 :param num: :param env: :param data_path: :return: """ _expr = \ """max((node_filesystem_size_bytes{env="ENV", mountpoint="DATA_PATH"}-node_filesystem_free_bytes{env="ENV", mountpoint="DATA_PATH"})*100/(node_filesystem_avail_bytes{ env="ENV",mountpoint="DATA_PATH"}+(node_filesystem_size_bytes{ env="ENV",mountpoint="DATA_PATH"}-node_filesystem_free_bytes{ env="ENV",mountpoint="DATA_PATH"})))by(instance,env)>= """ return _expr.replace("ENV", env).replace( "DATA_PATH", data_path).replace("\n", "").replace( " ", "" ) + str(num) @staticmethod def get_service_port(service_name): port = MONITOR_PORT.get(service_name) return port def make_data_node_rule(self, level, data_path, env="default"): """ 生成数据分区告警规则 :param level: 告警级别 :param data_path: 数据分区路径 :param env: 主机所属环境 :return: """ try: from db_models.models import HostThreshold warning_obj = HostThreshold.objects.filter(env_id=1, index_type="disk_data_used", alert_level="warning").first() critical_obj = HostThreshold.objects.filter(env_id=1, index_type="disk_data_used", alert_level="critical").first() warning_threshold = warning_obj.condition_value if warning_obj else "0" critical_threshold = critical_obj.condition_value if critical_obj else "100" except Exception as e: warning_threshold = "80" critical_threshold = "90" logger.error(f"更新数据分区告警于是失败!详情为:{e}") des = "主机 {{ $labels.instance }} 数据分区使用率为 " \ "{{ $value | humanize }}%, 大于阈值 " return { "alert": "主机数据分区磁盘使用率过高", "annotations": { "disk_data_path": f"{data_path}", "consignee": f"{self.email_address}", "description": des + f"{critical_threshold}%" if level == "critical" else des + f"{warning_threshold}%", "summary": "disk_data_used (instance {{ $labels.instance }})" }, "expr": self.get_expr( critical_threshold if level == "critical" else warning_threshold, env, data_path), "for": "1m", "labels": { "job": "nodeExporter", "severity": level } } def update_node_data_rule(self, data_path, env="default"): """ 更新主机data disk分区告警规则,当添加主机时使用 :rtype: tuple :param data_path: 数据分区地址 :param env: 主机所属环境 :return: """ logger.info(f"Start update_node_data_rule: {data_path}; {env}") data_rule_path = os.path.join( self.prometheus_rules_path, f"{env}_node_data_rule.yml") _critical = self.make_data_node_rule("critical", data_path, env) _warning = self.make_data_node_rule("warning", data_path, env) # _critical_flag = True # _warning_flag = True if not os.path.exists(data_rule_path): node_data_rule_dic = { "groups": [ { "name": "主机数据分区磁盘使用率过高", "rules": [ _critical, _warning ] } ] } self.write_dic_to_yaml(node_data_rule_dic, data_rule_path) return True, "success by new create" node_data_rule_dic = self.get_dic_from_yaml(data_rule_path) groups = node_data_rule_dic.get("groups", []).copy() for item_index, item in enumerate(groups): rules = item.get("rules", []).copy() for el_index, el in enumerate(rules): if el.get("annotations", {}).get("disk_data_path") \ == data_path and el.get("labels", {}).get("severity") \ == "critical": # _critical_flag = False item.get("rules", [])[el_index] = _critical if el.get("annotations", {}).get("disk_data_path") \ == data_path and el.get("labels", {}).get( "severity") \ == "warning": # _warning_flag = False item.get("rules", [])[el_index] = _warning node_data_rule_dic.get("groups", [])[item_index] = item self.write_dic_to_yaml(node_data_rule_dic, data_rule_path) return True, "success" def add_rules(self, rule_type, env="default"): """ rule_type: node: 主机告警规则 service: 服务状态告警规则 exporter: exporter状态告警规则 更新prometheus rules规则, 建议在安装prometheus时进行更新 :param rule_type: 更新方式 :param env: 环境信息 :return: """ logger.info(f"Start add rules: {rule_type}; {env}") rules_file_placeholder_script = [ {"ENV": env}, {"EMAIL_ADDRESS": self.email_address} ] if rule_type == "node": node_rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_node_rule.yml" ) if not os.path.exists(node_rule_yml_file): shutil.copy( self.prometheus_node_rule_tpl, node_rule_yml_file ) self.replace_placeholder( node_rule_yml_file, rules_file_placeholder_script ) elif rule_type == "service": status_rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_service_status_rule.yml" ) if not os.path.exists(status_rule_yml_file): shutil.copy( self.prometheus_service_status_rule_tpl, status_rule_yml_file ) self.replace_placeholder( status_rule_yml_file, rules_file_placeholder_script ) elif rule_type == "exporter": exporter_rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_exporter_status_rule.yml") if not os.path.exists(exporter_rule_yml_file): shutil.copy( self.prometheus_exporter_status_rule_tpl, exporter_rule_yml_file ) self.replace_placeholder( exporter_rule_yml_file, rules_file_placeholder_script ) else: return False, "not support!" return True, "success" def delete_rules(self, rule_type, env="default"): """ 删除prometheus rules的规则 :param rule_type: 删除类型 :param env: 环境名称 :return: """ if rule_type not in ("node", "service", "exporter"): return False, "not support!" if rule_type == "node": rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_node_rule_yml" ) else: rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_{rule_type}_status_rule.yml" ) if os.path.exists(rule_yml_file): os.remove(rule_yml_file) return True, "success" def add_node(self, nodes_data): """ nodes_data = [ { "data_path": "/data", "env": "default", "ip": "127.0.0.1", "instance_name": "instance" } ] 添加主机到自监控系统 :param nodes_data: 新增的主机信息 :return: """ logger.info(f"Start add node: {nodes_data}") if not nodes_data: return False, "nodes_data can not be null" node_target_list = list() # 遍历主机数据,添加主机层的告警规则 for item in nodes_data: node_target_ele = { "targets": [item["ip"] + ":" + str(self.monitor_port)], "labels": { "instance": item["ip"], "instance_name": "{}".format(item.get("instance_name")), "service_type": "host", "env": item["env"]} } node_target_list.append(node_target_ele) print("添加数据分区", item["data_path"]) # 需要像数据库添加数据分区的的规则 if item["data_path"]: self.add_data_disk_rules(item["data_path"], item["env"]) # # 更新主机node rule # self.add_rules("node", item["env"]) # # 更新exporter的告警规则 # self.add_rules("exporter", item["env"]) # # 更新数据分区的告警规则 # if item["data_path"]: # self.update_node_data_rule(item["data_path"], item["env"]) # 增加主机的target配置文件(prometheus/conf/targets) if os.path.exists(self.node_exporter_targets_file): with open(self.node_exporter_targets_file, "r") as f: content = f.read() if content: old_node_target_list = json.loads(content) node_target_list.extend(old_node_target_list) node_target_list = self.json_distinct(node_target_list) with open(self.node_exporter_targets_file, "w") as f2: json.dump(node_target_list, f2, ensure_ascii=False, indent=4) self.reload_prometheus() return True, "success" def delete_node(self, nodes_data): """ 从自监控系统删除主机信息 :param nodes_data: 要删除的主机信息 :return: """ if not nodes_data: return False, "nodes_data can not be null" if os.path.exists(self.node_exporter_targets_file): with open(self.node_exporter_targets_file, "r") as f: content = f.read() if content: node_target_list = json.loads(content) else: node_target_list = list() else: return False, f"{self.node_exporter_targets_file} not exists!" if not node_target_list: return True, "success" for item in nodes_data: ip = item["ip"] for node in node_target_list: if ip in node["labels"]["instance"]: node_target_list.remove(node) break with open(self.node_exporter_targets_file, "w") as f2: json.dump(node_target_list, f2, ensure_ascii=False, indent=4) return True, "success" def update_agent_service(self, dest_ip, action, services_data): """ 接收omp传来的参数,解析后发送到monitor_agent :param action: 更新动作 add or delete :param services_data: 要更新的服务信息 :param dest_ip: :return: """ json_dict = dict() json_content = list() headers = CW_TOKEN dest_url = '' if action == 'add': dest_url = 'http://{}:{}/update/service/add'.format(dest_ip, self.monitor_port) # NOQA for sd in services_data: service_temp_data = dict() service_temp_data['service_port'] = sd.get('listen_port') service_temp_data['run_port'] = sd.get('run_port') if sd.get('service_name') in EXPORTERS: service_temp_data['exporter_port'] = self.get_service_port( '{}Exporter'.format(sd.get('service_name'))) else: service_temp_data['exporter_port'] = sd.get( 'metric_port', 0) if sd.get('service_name') in METRICS.keys(): service_temp_data['exporter_metric'] = METRICS.get( sd.get('service_name'), 0) else: service_temp_data['exporter_metric'] = 'metrics' service_temp_data['username'] = sd.get('username', '') service_temp_data['password'] = sd.get('password', '') service_temp_data['name'] = sd.get('service_name') service_temp_data['only_process'] = sd.get('only_process') service_temp_data['process_key_word'] = sd.get( 'process_key_word') service_temp_data['instance'] = dest_ip service_temp_data['env'] = sd.get('env') service_temp_data['log_path'] = sd.get('log_path') service_temp_data["scrape_log_level"] = LOKI_CONFIG.get( "scrape_log_level") json_content.append(service_temp_data) elif action == 'delete': dest_url = 'http://{}:{}/update/service/delete'.format(dest_ip, self.monitor_port) # NOQA for sd in services_data: json_content.append(sd.get('service_name')) json_dict['services'] = json_content try: logger.info(f'向agent发送数据{json_dict}') result = requests.post( dest_url, headers=headers, data=json.dumps(json_dict)).json() if result.get('return_code') == 0: logger.info('向{}更新服务{}配置成功!'.format( dest_ip, services_data[0].get('service_name'))) else: logger.error('向{}更新服务{}配置失败!'.format( dest_ip, services_data[0].get('service_name'))) # return False, result.get('return_message') except Exception as e: logger.error('向{}更新服务{}配置失败!'.format( dest_ip, services_data[0].get('service_name'))) logger.error(e) # return False, e try: from utils.parse_config import MONITOR_PORT, LOCAL_IP from db_models.models import Host update_agent_promtail_url = f'http://{dest_ip}:{self.monitor_port}/update/promtail/add' # NOQA host_agent_dir = Host.objects.filter( ip=dest_ip).values_list('agent_dir', flat=True)[0] json_dict['promtail_config'] = { 'http_listen_port': MONITOR_PORT.get('promtail'), 'loki_url': f'http://{LOCAL_IP}:{MONITOR_PORT.get("loki")}/loki/api/v1/push' # NOQA } json_dict['agent_dir'] = host_agent_dir logger.info(f'向agent发送数据{json_dict}') promtail_result = requests.post( update_agent_promtail_url, headers=headers, data=json.dumps(json_dict)).json() if promtail_result.get('return_code') == 0: logger.info('向{}更新服务{}日志监控配置成功!'.format( dest_ip, services_data[0].get('service_name'))) else: logger.error('向{}更新服务{}日志监控配置失败!'.format( dest_ip, services_data[0].get('service_name'))) # return False, promtail_result.get('return_message') except Exception as e: logger.error(e) logger.error('向{}更新服务{}日志监控失败!'.format( dest_ip, services_data[0].get('service_name'))) # return False, e return True, 'success' def add_service(self, service_data): """ service_data = { "service_name": "mysql", "instance_name": "mysql_dosm", "data_path": "/data/appData/mysql", "log_path": "/data/logs/mysql", "env": "default", "ip": "127.0.0.1", "listen_port": "3306" "metric_port": "19018" } 添加有exporter的组件信息到各自的exporter监控 :param service_data: 新增的服务信息 :return: """ if not service_data: return False, "args cant be null" logger.info(f'收到信息:{service_data}') job_name_str = "{}Exporter".format(service_data.get('service_name')) prom_job_dict = { "job_name": job_name_str, "metrics_path": f"/metrics/monitor/{service_data.get('service_name')}", "file_sd_configs": [ { "refresh_interval": "30s", "files": [ f"targets/{service_data.get('service_name')}Exporter_all.json" ] } ] } with open(self.prometheus_conf_path, "r") as fr: content = yaml.load(fr.read(), yaml.Loader) content.get("scrape_configs").append(prom_job_dict) content["scrape_configs"] = self.json_distinct( content.get("scrape_configs")) with open(self.prometheus_conf_path, "w", encoding="utf8") as fw: yaml.dump(data=content, stream=fw, allow_unicode=True, sort_keys=False) self_exporter_target_file = os.path.join(self.prometheus_targets_path, "{}Exporter_all.json".format(service_data["service_name"])) self_target_list = list() self_target_ele = "" try: self_target_ele = { "labels": { "instance": "{}".format(service_data["ip"]), "instance_name": "{}".format(service_data.get("instance_name")), "service_type": "service", "env": "{}".format(service_data["env"]) }, "targets": [ "{}:{}".format(service_data["ip"], self.monitor_port) ] } except KeyError as func_e: logger.error(func_e) self_target_list.append(self_target_ele) if os.path.exists(self_exporter_target_file): with open(self_exporter_target_file, 'r') as f: content = f.read() if content: old_self_target_list = json.loads(content) self_target_list.extend(old_self_target_list) self_target_list = self.json_distinct(self_target_list) with open(self_exporter_target_file, 'w') as f2: json.dump(self_target_list, f2, ensure_ascii=False, indent=4) flag, msg = self.update_agent_service( service_data.get('ip'), 'add', [service_data]) if not flag: return False, msg # self.add_rules('service', service_data.get('env')) reload_prometheus_url = 'http://localhost:19011/-/reload' # TODO 确认重载prometheus动作在哪执行 try: requests.post(reload_prometheus_url, auth=self.basic_auth) except Exception as e: logger.error(e) logger.error("重载prometheus配置失败!") return True, "success" def delete_service(self, service_data): """ 从自有的exporter中删除对应的服务信息 :param service_data: :return: """ if not service_data: return False, "args cant be null" self_exporter_target_file = os.path.join(self.prometheus_targets_path, "{}Exporter_all.json".format(service_data["service_name"])) self_target_list = list() if os.path.exists(self_exporter_target_file): with open(self_exporter_target_file, 'r') as f: content = f.read() if content: self_target_list = json.loads(content) else: logger.error("{}不存在!".format(self_exporter_target_file)) return False, "Failed" try: instance = service_data['ip'] env = service_data['env'] for service in self_target_list: if (instance == service["labels"]["instance"]) and (env == service["labels"]["env"]): self_target_list.remove(service) except KeyError as func_e: logger.error(func_e) with open(self_exporter_target_file, 'w') as f2: json.dump(self_target_list, f2, ensure_ascii=False, indent=4) flag, msg = self.update_agent_service( service_data.get('ip'), 'delete', [service_data]) if not flag: return False, msg reload_prometheus_url = 'http://localhost:19011/-/reload' # TODO 确认重载prometheus动作在哪执行 try: requests.post(reload_prometheus_url, auth=self.basic_auth) except Exception as e: logger.error(e) logger.error("重载prometheus配置失败!") return True, "success" def update_host_threshold(self, env="default", env_id=1): rules_file_placeholder_script = [ {"ENV": env}, {"EMAIL_ADDRESS": self.email_address} ] node_rule_yml_file = os.path.join( self.prometheus_rules_path, f"{env}_node_rule.yml" ) if not os.path.exists(node_rule_yml_file): shutil.copy( self.prometheus_node_rule_tpl, node_rule_yml_file ) self.replace_placeholder( node_rule_yml_file, rules_file_placeholder_script ) from utils.prometheus.update_threshold import config_update try: # 调用自监控脚本更新环境阈值 host_thresholds = HostThreshold.objects.filter( env_id=env_id).values( 'index_type', 'condition', 'condition_value', 'alert_level') services_objs = ServiceCustomThreshold.objects.filter( env_id=env_id ).order_by("service_name", "index_type", "condition_value").values( "service_name", "index_type", "condition", "condition_value", "alert_level") services_dict = {} for services_obj in services_objs: service_name = services_obj.pop("service_name") info = services_dict.get(service_name) if not info: services_dict[service_name] = [services_obj] else: services_dict[service_name].append(services_obj) params = { 'env_name': env, 'hosts': list(host_thresholds), 'services': services_dict } # data_dir = get_path_dir(env_id) # if data_dir: # params.update(disk_data_path=data_dir) # TODO 补充替换数据分区阈值的逻辑 update_result = config_update(params) if not update_result: return False, "failed" return True, "success" except Exception as e: import traceback logger.error(f"同步监控指标出错:{e}{traceback.format_exc()}") return False, "failed" def gen_one_rule(self, **kwargs): # NOQA """ 生成单个规则 """ expr = kwargs.get("expr") compare_str = kwargs.get("compare_str") for_time = kwargs.get("for_time") alert = kwargs.get("alert") summary = kwargs.get("summary") description = kwargs.get("description") threshold_value = kwargs.get("threshold_value") expr_data = f"{expr} {compare_str} {threshold_value}" labels = kwargs.get("labels") one_rule = { "alert": alert, "annotations": { "description": description, "summary": summary, }, "expr": expr_data, "for": for_time, "labels": labels } return one_rule def get_hash_value(self, expr, severity): # NOQA data = expr + severity hash_data = hashlib.md5(data.encode(encoding='UTF-8')).hexdigest() return hash_data def add_data_disk_rules(self, data_path, env="default"): """ """ # threshold = "80" if level == "warning" else 90 logger.error("开始添加数据分区规则") threshold_list = [("warning", "80"), ("critical", "90")] for i in range(2): info = threshold_list[i] level = info[0] threshold = info[1] expr = 'max((node_filesystem_size_bytes{env="ENV",' \ 'mountpoint="DATA_PATH"}-node_filesystem_free_bytes{env="ENV",' \ 'mountpoint="DATA_PATH"})*100/(node_filesystem_avail_bytes{' \ 'env="ENV",mountpoint="DATA_PATH"}+(node_filesystem_size_bytes{' \ 'env="ENV",mountpoint="DATA_PATH"}-node_filesystem_free_bytes{' \ 'env="ENV",mountpoint="DATA_PATH"}))) by (instance,env)'.replace( "ENV", env).replace("DATA_PATH", data_path) data = { "alert": f"主机数据分区{data_path}磁盘使用率", "description": '主机 {{ $labels.instance }} 数据分区使用率为 {{ $value | humanize }}%, 大于阈值 $threshold$%'.replace( "$threshold$", threshold), "expr": expr, "compare_str": ">=", "threshold_value": threshold, "for_time": "60s", "severity": level, "labels": { "job": "nodeExporter", "severity": level }, "name": "数据分区使用率", "quota_type": 0, "status": 1, "service": "node", "forbidden": 2, } hash_data = self.get_hash_value(expr=expr, severity=level) if AlertRule.objects.filter(expr=expr, severity=level, hash_data=hash_data).exists(): continue try: data.update(hash_data=hash_data) AlertRule(**data).save() except Exception as e: logger.error(f"更新数据分区错误:{e}") self.update_rule_file(env=env) return True def update_rule_file(self, add_data=None, add=False, update=False, delete=False, env="default", rule_id=0, env_id=1): """ 更新规则文件 """ rule_file_path = os.path.join( self.prometheus_rules_path, f"{env}_rule.yml") try: all_rules = AlertRule.objects.filter(status=1).all() if delete or update: all_rules = AlertRule.objects.filter( status=1).exclude(id=rule_id).all() init_data = {"groups": [ { "name": "OMP Alert", "rules": [] } ]} for rule in all_rules: content = { "alert": rule.alert, "annotations": { "description": rule.description, "summary": rule.summary, }, "expr": f"{rule.expr} {rule.compare_str} {rule.threshold_value}", "for": rule.for_time, "labels": rule.labels, } init_data["groups"][0]["rules"].append(content) if add or update: init_data["groups"][0]["rules"].append( self.gen_one_rule(**add_data) ) my_yaml = YAML() with open(rule_file_path, "w", encoding="utf8") as fp: my_yaml.dump(init_data, fp) return True except Exception as e: logger.error(f"生成规则文件{rule_file_path}失败{e}") return False def reload_prometheus(self): """ 重载prometheus """ reload_prometheus_url = 'http://localhost:19011/-/reload' # TODO 确认重载prometheus动作在哪执行 try: response = requests.post( reload_prometheus_url, auth=self.basic_auth) logger.error(f"重载成功 {response.text}") return True except Exception as e: logger.error(e) logger.error("重载prometheus配置失败!") return False ================================================ FILE: omp_server/promemonitor/tasks.py ================================================ # -*- coding: utf-8 -*- # Project: tasks # Author: jon.liu@yunzhihui.com # Create time: 2021-10-09 09:17 # IDE: PyCharm # Version: 1.0 # Introduction: """ 监控端异步任务 """ import os import logging import traceback from celery import shared_task from celery.utils.log import get_task_logger from db_models.models import Host from utils.plugin.salt_client import SaltClient # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") def real_monitor_agent_restart(host_obj): """ 重启监控Agent :param host_obj: 主机对象 :type host_obj Host :return: """ logger.info( f"Restart Monitor Agent for {host_obj.ip}, Params: " f"username: {host_obj.username}; " f"port: {host_obj.port}; " f"install_dir: {host_obj.agent_dir}!") salt_obj = SaltClient() _script_path = os.path.join( host_obj.agent_dir, "omp_monitor_agent/monitor_agent.sh") flag, message = salt_obj.cmd( target=host_obj.ip, command=f"bash {_script_path} restart", timeout=60 ) logger.info( f"Restart monitor agent for {host_obj.ip}: " f"get flag: {flag}; get res: {message}") if flag: Host.objects.filter(ip=host_obj.ip).update(monitor_agent=0) else: Host.objects.filter(ip=host_obj.ip).update( monitor_agent=2, monitor_agent_error=str(message)[:200] if len( str(message)) > 200 else str(message) ) @shared_task def monitor_agent_restart(host_id): """ 主机Agent的重启操作 :param host_id: 主机的id :return: """ try: host_obj = Host.objects.get(id=host_id) real_monitor_agent_restart(host_obj=host_obj) except Exception as e: logger.error( f"Restart Monitor Agent For {host_id} Failed with error: " f"{str(e)};\ndetail: {traceback.format_exc()}" ) Host.objects.filter(id=host_id).update( monitor_agent=2, monitor_agent_error=str(e)) ================================================ FILE: omp_server/promemonitor/urls.py ================================================ """ 监控相关的路由 """ from rest_framework.routers import DefaultRouter from promemonitor.custom_script_views import CustomScriptViewSet, CustomScriptJobInfoView from promemonitor.views import ( MonitorUrlViewSet, ListAlertViewSet, UpdateAlertViewSet, MaintainViewSet, ReceiveAlertViewSet, MonitorAgentRestartView, GrafanaUrlViewSet, InstanceNameListView, InstrumentPanelView, GetSendEmailConfig, UpdateSendEmailConfig, GetSendAlertSettingView, UpdateSendAlertSettingView, HostThresholdView, ServiceThresholdView, CustomThresholdView, BuiltinsRuleView, QuotaView, PromSqlTestView, BatchUpdateRuleView ) router = DefaultRouter() router.register(r'monitorurl', MonitorUrlViewSet) router.register(r'listAlert', ListAlertViewSet, basename='listAlert') router.register(r'updateAlert', UpdateAlertViewSet, basename='updateAlert') router.register(r'restartMonitorAgent', MonitorAgentRestartView, basename="restartMonitorAgent") router.register("globalMaintain", MaintainViewSet, basename='globalMaintain') router.register(r'receiveAlert', ReceiveAlertViewSet, basename='receiveAlert') router.register(r'grafanaurl', GrafanaUrlViewSet, basename="grafanaurl") router.register(r'instanceNameList', InstanceNameListView, basename='instanceNameList') router.register(r"instrumentPanel", InstrumentPanelView, basename="instrumentPanel") router.register(r'getSendEmailConfig', GetSendEmailConfig, basename='getSendEmailConfig') router.register(r'updateSendEmailConfig', UpdateSendEmailConfig, basename='updateSendEmailConfig') router.register(r'getSendAlertSetting', GetSendAlertSettingView, basename='getSendAlertSetting') router.register(r'updateSendAlertSetting', UpdateSendAlertSettingView, basename='updateSendAlertSetting') router.register(r'hostThreshold', HostThresholdView, basename='hostThreshold') router.register(r'serviceThreshold', ServiceThresholdView, basename='serviceThreshold') router.register(r'customThreshold', CustomThresholdView, basename='customThreshold') router.register(r'customScript', CustomScriptViewSet, basename='customScript') router.register(r'customScriptJobInfo', CustomScriptJobInfoView, basename='customScriptJobInfo') router.register(r'builtinRule', BuiltinsRuleView, basename="builtinRule") router.register(r'quota', QuotaView, basename="quota") router.register(r'testPromSql', PromSqlTestView, basename="testPromSql") router.register(r'batchUpdateRule', BatchUpdateRuleView, basename="batchUpdateRule") urlpatterns = router.urls ================================================ FILE: omp_server/promemonitor/views.py ================================================ # Create your views here. """ 监控相关视图 """ import json import logging import traceback import requests from django.core.validators import EmailValidator from django.db import transaction from django.db.models import F from django.shortcuts import get_object_or_404 from django_filters.rest_framework import DjangoFilterBackend from rest_framework.decorators import action from rest_framework.filters import OrderingFilter from rest_framework.mixins import ( ListModelMixin, CreateModelMixin, DestroyModelMixin, UpdateModelMixin ) from rest_framework.response import Response from rest_framework.serializers import Serializer from rest_framework.viewsets import GenericViewSet from db_models.models import ( Host, MonitorUrl, Alert, Maintain, ApplicationHub, Service, EmailSMTPSetting, AlertSendWaySetting, HostThreshold, ServiceThreshold, ServiceCustomThreshold, Rule, AlertRule, Env ) from omp_server.settings import CUSTOM_THRESHOLD_SERVICES from promemonitor import grafana_url from promemonitor.alert_util import utc_to_local, get_monitor_url, get_log_url from promemonitor.promemonitor_filters import AlertFilter, MyTimeFilter, \ QuotaFilter from promemonitor.promemonitor_serializers import ( MonitorUrlSerializer, ListAlertSerializer, UpdateAlertSerializer, MaintainSerializer, MonitorAgentRestartSerializer, ReceiveAlertSerializer, RuleSerializer, QuotaSerializer ) from promemonitor.prometheus import Prometheus from utils.common.exceptions import OperateError from utils.common.paginations import PageNumberPager from utils.parse_config import PROMETHEUS_AUTH from promemonitor.prometheus_utils import PrometheusUtils logger = logging.getLogger('server') class MonitorUrlViewSet(ListModelMixin, CreateModelMixin, GenericViewSet): """ list: 查询监控地址列表 create: 创建一批监控配置 multiple_update: 更新一个或多个监控配置一个或多个字段 """ serializer_class = MonitorUrlSerializer queryset = MonitorUrl.objects.all() get_description = "查询监控地址配置" patch_description = "修改监控地址配置" def get_serializer(self, *args, **kwargs): serializer_class = self.get_serializer_class() kwargs.setdefault('context', self.get_serializer_context()) if self.request: if isinstance(self.request.data.get("data"), list): return serializer_class(many=True, *args, **kwargs) return serializer_class(*args, **kwargs) else: return serializer_class(*args, **kwargs) @action(methods=['patch'], detail=False) def multiple_update(self, request, *args, **kwargs): partial = kwargs.pop('partial', True) instances = [] for item in request.data.get('data'): instance = get_object_or_404(MonitorUrl, id=int(item['id'])) serializer = super().get_serializer(instance, data=item, partial=partial) serializer.is_valid(raise_exception=True) serializer.save() instances.append(serializer.data) return Response(instances) class GrafanaUrlViewSet(ListModelMixin, GenericViewSet): """ list: 查询异常清单列表 """ queryset = MonitorUrl.objects.all() def list(self, request, *args, **kwargs): params = request.query_params.dict() asc = params.pop('asc', '0') asc = True if asc == '0' else False ordering = params.pop('ordering', 'date') current = grafana_url.explain_prometheus(params) if current == "error": raise OperateError("prometheus获取数据失败,请检查prometheus状态") prometheus_info = sorted( current, key=lambda e: e.__getitem__(ordering), reverse=asc) # prometheus_json = json.dumps(prometheus_info, ensure_ascii=False) return Response(prometheus_info) class ListAlertViewSet(ListModelMixin, GenericViewSet): """ 获取告警记录列表视图类 """ serializer_class = ListAlertSerializer queryset = Alert.objects.all().order_by("-create_time") # 分页,过滤,排序 pagination_class = PageNumberPager filter_backends = ( DjangoFilterBackend, OrderingFilter, MyTimeFilter, ) filter_class = AlertFilter ordering_fields = ("alert_host_ip", "alert_instance_name", "alert_time") class UpdateAlertViewSet(CreateModelMixin, GenericViewSet): """ 更新告警记录视图类 """ serializer_class = UpdateAlertSerializer queryset = Alert.objects.all().order_by('id') post_description = "更新告警记录(已读/未读)" class MaintainViewSet(GenericViewSet, CreateModelMixin, ListModelMixin): """ create: 全局进入 / 退出维护模式 """ queryset = Maintain.objects.filter( matcher_name='env', matcher_value='default') serializer_class = MaintainSerializer # 操作信息描述 post_description = "更新全局维护状态" class ReceiveAlertViewSet(GenericViewSet, CreateModelMixin): """ 接收alertmanager发送过来的告警消息后解析入库 """ queryset = None serializer_class = ReceiveAlertSerializer # 操作信息描述 post_description = "接收alertmanager告警信息" # 关闭权限、认证设置 authentication_classes = () permission_classes = () class MonitorAgentRestartView(GenericViewSet, CreateModelMixin): """ create: 重启监控Agent接口 """ queryset = Host.objects.filter(is_deleted=False) serializer_class = MonitorAgentRestartSerializer # 操作信息描述 post_description = "重启监控Agent" class InstanceNameListView(GenericViewSet, ListModelMixin): """ 返回主机和服务实例名列表 """ # 操作信息描述 post_description = "返回主机和服务实例名列表" def list(self, request, *args, **kwargs): alert_instance_name_list = list() host_instance_name_list = list(Host.objects.all().values_list( 'instance_name', flat=True)) service_instance_name_list = [] # TODO 待应用模型完善 alert_instance_name_list.append(host_instance_name_list) alert_instance_name_list.append(service_instance_name_list) return Response(alert_instance_name_list) class InstrumentPanelView(GenericViewSet, ListModelMixin): """ 返回仪表盘所需数据 """ # 操作信息描述 get_description = "查询仪表盘数据" @staticmethod def get_prometheus_alerts(): """ 请求prometheus alerts接口返回告警内容 """ mu = MonitorUrl.objects.filter(name="prometheus").first() prometheus_url = mu.monitor_url if mu else "127.0.0.1:19011" prometheus_auth = (PROMETHEUS_AUTH.get( "username", "omp"), PROMETHEUS_AUTH.get("plaintext_password", "")) try: prometheus_alerts_url = f"http://{prometheus_url}/api/v1/alerts" # NOQA response = requests.get(prometheus_alerts_url, headers={ }, data="", auth=prometheus_auth) return True, json.loads(response.text) except Exception as e: logger.error("prometheus请求alerts失败:" + str(e)) return False, "Failed" def get_exc_serializer_info(self): host_info_dict = {} database_info_dict = {} service_info_dict = {} component_info_dict = {} third_info_dict = {} host_info_list = [] database_info_list = [] service_info_list = [] component_info_list = [] third_info_list = [] host_ip_list = [] host_list = Host.objects.values("ip", "instance_name") host_ip_list = [host.get("ip") for host in host_list] ignore_status_list = [Service.SERVICE_STATUS_NORMAL, Service.SERVICE_STATUS_STARTING, Service.SERVICE_STATUS_STOPPING, Service.SERVICE_STATUS_RESTARTING, Service.SERVICE_STATUS_STOP] database_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_COMPONENT).filter( service__app_labels__label_name__contains="数据库").filter( service_status__in=ignore_status_list).filter( service__is_base_env=False) service_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_SERVICE).filter( service_status__in=ignore_status_list).filter( service__is_base_env=False).filter( service_controllers__start__isnull=False) component_list = Service.objects.filter( service__app_type=ApplicationHub.APP_TYPE_COMPONENT).filter( service_status__in=ignore_status_list).filter( service__is_base_env=False) # third_info_all = None # TODO 暂为空 host_info_all_count = len(host_list) database_info_all_count = len(database_list) service_info_all_count = len(service_list) component_info_all_count = len(component_list) third_info_all_count = 0 # TODO 暂为空 # host_info_exc_count = 0 database_info_exc_count = 0 service_info_exc_count = 0 component_info_exc_count = 0 third_info_exc_count = 0 host_info_no_monitor_count = 0 database_info_no_monitor_count = 0 service_info_no_monitor_count = 0 component_info_no_monitor_count = 0 third_info_no_monitor_count = 0 flag, alert_data = self.get_prometheus_alerts() error_database_list = list() error_service_list = list() error_component_list = list() error_host_list = list() if flag: alerts = alert_data.get('data').get('alerts') for ele in alerts: if not isinstance(ele, dict): continue if ele.get("status") == "resolved" or ele.get( "state") == "resolved" or ele.get("labels").get("instance") not in host_ip_list: continue _ip = ele.get("labels").get("instance", "") service_instance_str = ele.get("labels").get("instance_name", "") if len(service_instance_str) < 1: app_name_str = ele.get("labels").get("app") if ele.get("labels").get("app") else \ ele.get("labels").get("job", "").split("Exporter")[0] service_instance_str = f"{app_name_str}-{_ip.split('.')[2]}-{_ip.split('.')[3]}" if len(service_instance_str) < 1: continue ele_dict = { "ip": _ip, "instance_name": service_instance_str, "alertname": ele.get("labels").get("alertname"), "severity": ele.get("labels").get("severity"), "date": utc_to_local(ele.get("activeAt")), "describe": ele.get("annotations").get("description"), "monitor_url": get_monitor_url([{ "ip": ele.get("labels").get("instance"), "type": "service", "instance_name": ele.get("labels").get("instance_name", "") }]), "log_url": get_log_url([{ "ip": ele.get("labels").get("instance"), "type": "service", "instance_name": ele.get("labels").get("instance_name", "") }]) } if ele.get("labels").get("job") == "nodeExporter": ele_dict["monitor_url"] = get_monitor_url([{ "ip": ele.get("labels").get("instance"), "type": "host", "instance_name": "node" }]) ele_dict["log_url"] = get_log_url([{ "ip": ele.get("labels").get("instance"), "type": "host", "instance_name": "node" }]) host_info_list.append(ele_dict) error_host_list.append(ele.get("labels").get("instance")) if list(filter( lambda x: x.service_instance_name == service_instance_str, list(database_list))): error_database_list.append(service_instance_str) database_info_list.append(ele_dict) error_component_list.append(service_instance_str) component_info_list.append(ele_dict) elif list(filter( lambda x: x.service_instance_name == service_instance_str, list( service_list) )): error_service_list.append(service_instance_str) service_info_list.append(ele_dict) elif list(filter( lambda x: x.service_instance_name == service_instance_str, list( component_list) )): error_component_list.append(service_instance_str) component_info_list.append(ele_dict) else: continue for index, hil in enumerate(host_info_list): if hil.get("severity") == "warning": for i, h in enumerate(host_info_list): if index == i: continue if hil.get("ip") == h.get("ip") and hil.get( "alertname") == h.get("alertname") and h.get( "severity") == "critical": host_info_list.pop(index) break elif hil.get("severity") == "critical": for i, h in enumerate(host_info_list): if index == i: continue if hil.get("ip") == h.get("ip") and hil.get( "alertname") == h.get("alertname") and h.get( "severity") == "warning": host_info_list.pop(i) break _, host_targets = Prometheus().get_all_host_targets() for host in host_list: if host.get("ip") in error_host_list: continue host_dict = { "ip": host.get("ip"), "instance_name": host.get("instance_name"), "severity": "normal"} if host.get("ip") not in host_targets: host_dict = { "ip": host.get("ip"), "instance_name": host.get("instance_name"), "severity": "unmonitored"} host_info_list.append(host_dict) _, service_targets = Prometheus().get_all_service_targets() for database in database_list: if database.service_instance_name in error_database_list: continue database_dict = {"ip": database.ip, "instance_name": database.service_instance_name, "app_name": database.service.app_name, "severity": "normal"} database_ip_instance_name_str = \ f"{database.ip}_{database.service_instance_name}" if database_ip_instance_name_str not in service_targets: database_dict = {"ip": database.ip, "instance_name": database.service_instance_name, "app_name": database.service.app_name, "severity": "unmonitored"} database_info_list.append(database_dict) for service in service_list: if service.service_instance_name in error_service_list: continue service_dict = {"ip": service.ip, "instance_name": service.service_instance_name, "app_name": service.service.app_name, "severity": "normal"} service_ip_instance_name_str = f"{service.ip}_" \ f"{service.service_instance_name}" if service_ip_instance_name_str not in service_targets: service_dict = {"ip": service.ip, "instance_name": service.service_instance_name, "app_name": service.service.app_name, "severity": "unmonitored"} service_info_list.append(service_dict) for component in component_list: if component.service_instance_name in error_component_list: continue component_dict = {"ip": component.ip, "instance_name": component.service_instance_name, "app_name": component.service.app_name, "severity": "normal"} component_ip_instance_name_str = f"{component.ip}_" \ f"{component.service_instance_name}" if component_ip_instance_name_str not in service_targets: component_dict = {"ip": component.ip, "instance_name": component.service_instance_name, "app_name": component.service.app_name, "severity": "unmonitored"} component_info_list.append(component_dict) host_info_exc_count = len(set(error_host_list)) database_info_exc_count = len(set(error_database_list)) service_info_exc_count = len(set(error_service_list)) component_info_exc_count = len(set(error_component_list)) host_info_dict.update({ "host_info_all_count": host_info_all_count, "host_info_exc_count": host_info_exc_count, "host_info_no_monitor_count": host_info_no_monitor_count, "host_info_list": host_info_list }) database_info_dict.update({ "database_info_all_count": database_info_all_count, "database_info_exc_count": database_info_exc_count, "database_info_no_monitor_count": database_info_no_monitor_count, "database_info_list": database_info_list }) service_info_dict.update({ "service_info_all_count": service_info_all_count, "service_info_exc_count": service_info_exc_count, "service_info_no_monitor_count": service_info_no_monitor_count, "service_info_list": service_info_list }) component_info_dict.update({ "component_info_all_count": component_info_all_count, "component_info_exc_count": component_info_exc_count, "component_info_no_monitor_count": component_info_no_monitor_count, "component_info_list": component_info_list }) third_info_dict.update({ "third_info_all_count": third_info_all_count, "third_info_exc_count": third_info_exc_count, "third_info_no_monitor_count": third_info_no_monitor_count, "third_info_list": third_info_list }) serializer_info = { "host": host_info_dict, "database": database_info_dict, "service": service_info_dict, "component": component_info_dict, "third": third_info_dict, } return serializer_info def list(self, request, *args, **kwargs): result = self.get_exc_serializer_info() return Response(result) class GetSendEmailConfig(GenericViewSet, ListModelMixin): """ 获取邮件发送配置 """ get_description = "获取邮件发送配置" def list(self, request, *args, **kwargs): email_setting = EmailSMTPSetting.objects.first() if email_setting: return Response(data=email_setting.get_dict()) return Response({}) class UpdateSendEmailConfig(GenericViewSet, CreateModelMixin): """ 更新邮件发送配置 """ post_description = "更新邮件发送配置" serializer_class = Serializer def create(self, request, *args, **kwargs): email_host = request.data.get("host") if not email_host: return Response(data={"code": 1, "message": "请填写SMTP邮件服务器地址!"}) email_port = request.data.get("port") if not email_port or not isinstance(email_port, int): return Response( data={"code": 1, "message": "请检查所填的SMTP邮件服务器端口是否正确!"}) email_host_user = request.data.get("username") if not email_host_user: return Response(data={"code": 1, "message": "请填写SMTP邮件服务器发件箱!"}) try: EmailValidator()(email_host_user) except Exception as e: message = "SMTP邮件服务器发件箱格式错误!" logger.error(f"{message} 错误信息:{str(e)}") return Response(data={"code": 1, "message": message}) email_host_password = request.data.get("password") if not email_host_password: return Response(data={"code": 1, "message": "填写SMTP邮件服务器发件箱格式错误!"}) try: EmailSMTPSetting.objects.all().delete() email_setting = EmailSMTPSetting.objects.create( email_host=email_host, email_port=email_port, email_host_user=email_host_user, email_host_password=email_host_password ) except Exception as e: message = "修改email邮箱服务器信息出错!" logger.error(f"{message} 详细信息:{str(e)}") return Response(data={"code": 1, "message": message}) state, email_url = email_setting.update_setting_config() if not state: return Response(data={"code": 1, "message": "同步到Alert Manage失败!"}) return Response({}) class GetSendAlertSettingView(GenericViewSet, ListModelMixin): """ 获取监控邮箱收件配置 """ get_description = "获取监控邮箱收件配置" def list(self, request, *args, **kwargs): env_id = request.GET.get("env_id", 0) filter_kwargs = dict(env_id=env_id) way_name = request.GET.get("way_name") if way_name: filter_kwargs["way_name"] = way_name objs = AlertSendWaySetting.objects.filter(**filter_kwargs) data = dict() for obj in objs: data.update({obj.way_name: obj.get_self_dict()}) if "email" not in data: data["email"] = AlertSendWaySetting.get_v1_5_email_dict(env_id) return Response(data=data) class UpdateSendAlertSettingView(GenericViewSet, CreateModelMixin): """更新监控邮箱收件配置""" post_description = "更新监控邮箱收件配置" serializer_class = Serializer def create(self, request, *args, **kwargs): env_id = request.data.get("env_id") alert_setting, _ = AlertSendWaySetting.objects.get_or_create( env_id=env_id, way_name="email" ) used = request.data.get("used", False) emails = request.data.get("server_url", "") if emails: for email in emails.split(","): try: EmailValidator()(email) except Exception as e: message = f"收件箱{email}格式错误!" logger.error(f"{message} 错误信息:{str(e)}") return Response(data={"code": 1, "message": message}) AlertSendWaySetting.update_email_config(bool(used), emails) email_setting = EmailSMTPSetting.objects.first() if not email_setting: return Response( data={"code": 1, "message": "邮箱SMTP服务器未配置,配置后才可发送告警邮件!"}) state, email_url = email_setting.update_setting_config() if not state: return Response(data={"code": 1, "message": "同步到Alert Manage失败!请确保Alert " "Manage可用"}) return Response({}) class HostThresholdView(GenericViewSet, ListModelMixin, CreateModelMixin): """ 读写主机阈值 """ get_description = "读取主机阈值设置" post_description = "更新主机阈值设置" serializer_class = Serializer def list(self, request, *args, **kwargs): """ 获取主机监控指标项设置 """ env_id = request.GET.get('env_id') if not env_id: return Response(data={"code": 1, "message": "请确认请求参数中包含env_id"}) if not HostThreshold.objects.filter(env_id=env_id).exists(): return Response(data={"code": 1, "message": f"env {env_id}错误"}) host_thresholds = HostThreshold.objects.filter( env_id=env_id, index_type__in=["cpu_used", "memory_used", "disk_root_used", "disk_data_used"] ).annotate( value=F("condition_value"), level=F("alert_level") ).order_by("index_type", "level").values( "index_type", "condition", "value", "level") data = { "cpu_used": [], "memory_used": [], "disk_root_used": [], "disk_data_used": [] } for host_threshold in host_thresholds: data[host_threshold.get("index_type")].append(host_threshold) return Response(data=data) def create(self, request, *args, **kwargs): """ 更新主机指标项到自监控平台 """ try: logger.info(f"主机监控指标更新接口获取到的参数为: {request.data}") update_data = request.data.get("update_data", {}) env_id = request.data.get("env_id") if not update_data: return Response(data={"code": 1, "message": "无法正确解析到要更新的数据!"}) if env_id is None: return Response( data={"code": 1, "message": "请确认请求参数中包含env_id"}) # 同步阈值至prometheus主机告警规则文件中,并做配置检查 # if not check_prometheus(): # return Response(1, "无法连接到prometheus,更改阈值失败!") _obj_lst = list() hosts_list = list() for key, value in update_data.items(): for item in value: if not item["condition"] or not item["value"] or not item[ "level"]: continue _obj = HostThreshold() _obj.index_type = key _obj.condition = item["condition"] _obj.condition_value = item["value"] _obj.alert_level = item["level"] _obj.env_id = env_id _obj_lst.append(_obj) hosts_list.append({ 'index_type': key, 'condition': item["condition"], 'condition_value': item["value"], 'alert_level': item["level"] }) with transaction.atomic(): HostThreshold.objects.filter(env_id=env_id).delete() HostThreshold.objects.bulk_create(_obj_lst) from promemonitor.prometheus_utils import PrometheusUtils prometheus_util = PrometheusUtils() prometheus_util.update_host_threshold(env_id=env_id) data_disk_dir_list = list(Host.objects.all().values_list( "data_folder", flat=True).distinct()) for ele in data_disk_dir_list: prometheus_util.update_node_data_rule(ele) return Response({}) except Exception as e: logger.error(f"更新主机相关阈值过程中出错: {traceback.format_exc()}") return Response( data={"code": 1, "message": f"更新主机相关阈值过程中出错: {str(e)}!"}) class ServiceThresholdView(GenericViewSet, ListModelMixin, CreateModelMixin): """ 读写服务阈值 """ get_description = "读取服务阈值设置" post_description = "更新服务阈值设置" serializer_class = Serializer def list(self, request, *args, **kwargs): """ 获取服务阈值监控指标项设置 """ env_id = request.GET.get('env_id') if not env_id: return Response(data={"code": 1, "message": "请确认请求参数中包含env_id"}) if not ServiceThreshold.objects.filter(env_id=env_id).exists(): return Response(data={"code": 1, "message": f"env {env_id}错误"}) service_thresholds = ServiceThreshold.objects.filter( env_id=env_id, index_type__in=["service_active", "service_cpu_used", "service_memory_used"] ).annotate( value=F("condition_value"), level=F("alert_level") ).order_by("index_type", "level").values( "index_type", "condition", "value", "level") data = { "service_active": [], "service_cpu_used": [], "service_memory_used": [], } for service_threshold in service_thresholds: data[service_threshold.get("index_type")].append(service_threshold) return Response(data=data) def create(self, request, *args, **kwargs): """ 更新服务阈值监控指标项设置 """ try: logger.info(f"服务监控指标更新接口获取到的参数为: {request.data}") update_data = request.data.get("update_data", {}) env_id = request.data.get("env_id") if not update_data: return Response(data={"code": 1, "message": "无法正确解析到要更新的数据!"}) if env_id is None: return Response( data={"code": 1, "message": "请确认请求参数中包含env_id"}) _obj_lst = list() services_list = list() for key, value in update_data.items(): for item in value: if not item["condition"] or not item["value"] or not item[ "level"]: continue _obj = ServiceThreshold() _obj.index_type = key _obj.condition = item["condition"] _obj.condition_value = item["value"] _obj.alert_level = item["level"] _obj.env_id = env_id _obj_lst.append(_obj) services_list.append({ 'index_type': key, 'condition': item["condition"], 'condition_value': item["value"], 'alert_level': item["level"] }) with transaction.atomic(): ServiceThreshold.objects.filter(env_id=env_id).delete() ServiceThreshold.objects.bulk_create(_obj_lst) return Response({}) except Exception as e: logger.error(f"更新服务相关阈值过程中出错: {traceback.format_exc()}") return Response( data={"code": 1, "message": f"更新服务相关阈值过程中出错: {str(e)}!"}) class CustomThresholdView(GenericViewSet, ListModelMixin, CreateModelMixin): """ 读写自定义服务指标阈值 """ get_description = "读取自定义服务指标阈值设置" post_description = "更新自定义服务指标阈值设置" serializer_class = Serializer def list(self, request, *args, **kwargs): """ 暂时只有kafka_consumergroup_lag """ env_id = request.GET.get('env_id') if not env_id: return Response(data={"code": 1, "message": "请确认请求参数中包含env_id"}) service_thresholds = list( ServiceCustomThreshold.objects.filter( env_id=env_id ).annotate( value=F("condition_value"), level=F("alert_level") ).order_by("service_name", "index_type", "level").values( "service_name", "index_type", "condition", "value", "level") ) data = dict() for service_threshold in service_thresholds: service_name = service_threshold.get("service_name", "") index_type = service_threshold.get("index_type", "") if not service_name or not index_type: continue index_type_info = { "condition": service_threshold.get("condition"), "index_type": index_type, "level": service_threshold.get("level"), "value": service_threshold.get("value") } threshold_info = data.get(service_name, {}) if not threshold_info: data[service_name] = {index_type: [index_type_info]} else: index_type_infos = threshold_info.get(index_type) if not index_type_infos: threshold_info[index_type] = [index_type_info] else: threshold_info[index_type].append(index_type_info) return Response(data=data) def valid_kafka_kafka_consumergroup_lag(self, value): # NOQA if isinstance(value, int): return value > 0 return value.isdigit() and int(value) > 0 def create(self, request, *args, **kwargs): """ 更新服务阈值监控指标项设置 """ try: logger.info(f"自定义服务监控指标更新接口获取到的参数为: {request.data}") service_name = request.data.get("service_name", "") index_types = CUSTOM_THRESHOLD_SERVICES.get(service_name) if not index_types: return Response(data={"code": 1, "message": "暂不支持该服务定制化阈值!"}) index_type = request.data.get("index_type", "") if not index_type or index_type not in index_types: return Response(data={"code": 1, "message": "暂不支持该指标项!"}) index_type_info = request.data.get("index_type_info", []) if not index_type_info: return Response(data={"code": 1, "message": "无法正确解析到要更新的数据!"}) env_id = request.data.get("env_id") if not ServiceCustomThreshold.objects.filter( env_id=env_id).exists(): return Response( data={"code": 1, "message": f"env {env_id}不存在"}) # 后续需要增加对环境是否存在的判断 # 后续可能需要同步阈值至prometheus的rules文件中 # if not check_prometheus(): # return Response(1, "无法连接到prometheus,更改阈值失败!") _obj_lst = list() # 创建阈值 for index_type_value in index_type_info: condition = index_type_value.get("condition") level = index_type_value.get("level") value = index_type_value.get("value") valid = getattr( self, f"valid_{service_name}_{index_type}" )(value) if not valid: return Response( data={"code": 1, "message": "阈值更新的值不符合要求!"}) _obj = ServiceCustomThreshold( env_id=env_id, service_name=service_name, index_type=index_type, condition=condition, condition_value=value, alert_level=level ) _obj_lst.append(_obj) with transaction.atomic(): ServiceCustomThreshold.objects.filter( env_id=env_id, service_name=service_name, index_type=index_type ).delete() ServiceCustomThreshold.objects.bulk_create(_obj_lst) return Response({}) except Exception as e: logger.error(f"更新服务相关阈值过程中出错: {traceback.format_exc()}") return Response( data={"code": 1, "message": f"更新服务相关阈值过程中出错: {str(e)}!"}) class QuotaView(ListModelMixin, GenericViewSet, CreateModelMixin, DestroyModelMixin): """ 读写自定义服务指标阈值 """ get_description = "读取指标规则" post_description = "更新指标规则" delete_description = "删除指标规则" serializer_class = QuotaSerializer queryset = AlertRule.objects.all().order_by("-create_time") pagination_class = PageNumberPager filter_backends = ( DjangoFilterBackend, OrderingFilter, ) filter_class = QuotaFilter def create(self, request, *args, **kwargs): """ 添加监控指标项 env_id = models.IntegerField("环境id", default=1) expr = models.TextField("监控指标表达式,报警语法", null=False, blank=False) threshold_value = models.FloatField("阈值的数值", null=False, blank=False) compare_str = models.CharField("比较符", max_length=64) for_time = models.CharField("持续一段时间获取不到信息就触发告警", max_length=64) severity = models.CharField("告警级别", max_length=64) alert = models.TextField("标题,自定义摘要") service = models.CharField("指标所属服务名称", max_length=255) status = models.IntegerField("启用状态", default=0) quota_type = models.IntegerField("指标的类型", choices=TYPE, default=0) labels = models.JSONField("额外指定标签") description = models.TextField("描述, 告警指标描述", null=True) """ p = PrometheusUtils() compare_str_dict = { ">=": "大于或等于", ">": "大于", "==": "等于", "!=": "不等于", "<=": "小于或等于", "<": "小于" } try: env_id = request.data.get("env_id") if not env_id: return Response( data={"code": 1, "message": "请确认请求参数中包含env_id"}) env_name = Env.objects.filter(id=env_id).first().name logger.info(f"创建指标规则接口获取到的参数为: {request.data}") quota_type = request.data.get("quota_type") compare_str = request.data.get("compare_str") severity = request.data.get("severity") threshold_value = request.data.get("threshold_value") id = request.data.pop("id", 0) expr = request.data.get("expr") if quota_type == 0: """ 内置指标 """ builtins_quota = request.data.pop("builtins_quota", None) name = builtins_quota.get("name") expr = builtins_quota.get("expr") description = builtins_quota.get("description") cn_compare = compare_str_dict.get(compare_str) request.data["name"] = name request.data["service"] = builtins_quota.get("service") request.data["description"] = description.replace("$compare_str$", cn_compare).replace( "$threshold_value$", str(threshold_value)) request.data["expr"] = expr.replace("$env$", env_name) severity = request.data.get("severity") elif quota_type == 1: """ 自定义promsql """ pass elif quota_type == 2: """ 日志监控 """ pass else: return Response( data={"code": 1, "message": f"创建指标规则过程中出错: 未识别的规则类型"}) if request.data["service"] != "service": request.data["labels"] = { "job": '{}Exporter'.format(request.data["service"]), "severity": severity } else: request.data["labels"] = { "severity": severity } if id != 0: if AlertRule.objects.filter(expr=request.data["expr"], severity=severity).exclude(id=id).exists(): return Response(data={"code": 1, "message": f"更新指标规则过程中出错: " f"同一指标规则级别重复添加"}) if not p.update_rule_file(add_data=request.data, update=True, rule_id=id): return Response(data={"code": 1, "message": f"更新指标规则错误"}) AlertRule.objects.filter(id=id).update(**request.data) ok = p.reload_prometheus() if not ok: return Response(data={"code": 1, "message": "prometheus 重载规则失败,请手动重启prometheus进行重载"}) return Response() print(request.data["expr"], severity) if AlertRule.objects.filter(expr=request.data["expr"], severity=severity).exists(): return Response(data={"code": 1, "message": f"创建指标规则过程中出错: " f"同一指标规则级别重复添加"}) if not p.update_rule_file(add_data=request.data, add=True): return Response(data={"code": 1, "message": f"创建指标规则错误"}) AlertRule(**request.data).save() ok = p.reload_prometheus() if not ok: return Response(data={"code": 1, "message": "prometheus 重载规则失败,请手动重启prometheus进行重载"}) return Response() except Exception as e: logger.error(f"创建指标规则过程中出错: {traceback.format_exc()}") return Response( data={"code": 1, "message": f"创建指标规则过程中出错: {str(e)}!"}) def delete(self, request, *args, **kwargs): """ 删除规则 """ id = request.query_params.get("id") p = PrometheusUtils() if not p.update_rule_file(delete=True, rule_id=id): return Response(data={"code": 1, "message": f"删除指标规则时,更新配置文件失败"}) num, _ = AlertRule.objects.filter(id=id).delete() if num == 0: return Response(data={"code": 1, "message": "删除失败"}) ok = p.reload_prometheus() if not ok: return Response(data={"code": 1, "message": "prometheus " "重载规则失败,请手动重启prometheus进行重载"}) return Response() class BuiltinsRuleView(GenericViewSet, ListModelMixin): post_description = "获取内置指标列表" serializer_class = RuleSerializer queryset = Rule.objects.all() def list(self, request, *args, **kwargs): data = self.get_serializer(self.queryset, many=True).data services = set([i.get("service") for i in data]) r_data = {} for service in services: r_data[service] = [] for i in data: if service == i.get("service"): r_data[service].append(i) return Response(data=r_data) class PromSqlTestView(GenericViewSet, CreateModelMixin): post_description = "测试指标" serializer_class = Serializer def create(self, request, *args, **kwargs): expr = request.data.get("expr") ok, res = Prometheus().get_quota_res(quota=expr) if ok: return Response(data=res) return Response(data={"code": 1, "message": res}) class BatchUpdateRuleView(GenericViewSet, CreateModelMixin): """ 批量修改 """ post_description = "批量修改启用停用状态" serializer_class = QuotaSerializer queryset = AlertRule.objects.all() def create(self, request, *args, **kwargs): """ id list 批量修改接口 """ p = PrometheusUtils() ids = request.data.get("ids") status = request.data.get("status") for id in ids: AlertRule.objects.filter(id=id).update(status=status) if not p.update_rule_file(): return Response(data={"code": 1, "message": f"删除指标规则时,更新配置文件失败"}) ok = p.reload_prometheus() if not ok: return Response(data={"code": 1, "message": "prometheus 重载规则失败,请手动重启prometheus进行重载"}) return Response() ================================================ FILE: omp_server/service_upgrade/__init__.py ================================================ ================================================ FILE: omp_server/service_upgrade/admin.py ================================================ from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/service_upgrade/apps.py ================================================ from django.apps import AppConfig class ServiceUpgradeConfig(AppConfig): name = 'service_upgrade' ================================================ FILE: omp_server/service_upgrade/filters.py ================================================ from rest_framework.filters import BaseFilterBackend class RollBackHistoryFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): params = request.query_params.get("history_id", '') params = params.replace('\x00', '').replace('null', '') if not params: return queryset queryset = queryset.filter(history_id=int(params)) return queryset ================================================ FILE: omp_server/service_upgrade/handler/__init__.py ================================================ ================================================ FILE: omp_server/service_upgrade/handler/base.py ================================================ import logging import time from datetime import datetime from functools import reduce from django.conf import settings from db_models.models import UpgradeDetail, RollbackDetail, Service from utils.plugin.salt_client import SaltClient logger = logging.getLogger(__name__) def get_install_detail(detail, service, operation_uuid): install_history = service.detailinstallhistory_set.filter( install_step_status=2).last() logger.info(f"{service.service_instance_name}无detail记录或该服务升级前为安装失败") if not install_history: return None, None # 设置安装路径 install_args = install_history.install_detail_args install_folder = "" for item in install_args.get("install_args"): if item.get("key") == "base_dir": install_folder = item.get("default") break if not install_folder: logger.error("找不到服务安装目录!") return None, None setattr(service, "install_folder", install_folder) # 判断静态服务 setattr( service, "is_static", bool(not service.service_controllers.get("start")) ) # 升级的data path setattr( detail, "data_path", install_args.get("data_folder") ) # 安装的uuid setattr( service, "_uuid", operation_uuid ) return detail, service def load_upgrade_detail(upgrade_detail, operation_uuid): service = upgrade_detail.service upgrade_detail, service = get_install_detail( upgrade_detail, service, operation_uuid) if not upgrade_detail or not service: return None, None, None # 加载关联的升级对象 relation_details = list( UpgradeDetail.objects.filter( union_server=upgrade_detail.union_server, history_id=upgrade_detail.history_id ).exclude(id=upgrade_detail.id) ) return upgrade_detail, service, relation_details def load_rollback_detail(rollback_detail, operation_uuid): service = rollback_detail.upgrade.service # 安装成功记录 rollback_detail, service = get_install_detail( rollback_detail, service, operation_uuid) # 加载关联的升级对象 relation_details = list( RollbackDetail.objects.filter( upgrade__union_server=rollback_detail.upgrade.union_server, history_id=rollback_detail.history_id ).exclude(id=rollback_detail.id).exclude( history_id=rollback_detail.history.id ) ) return rollback_detail, service, relation_details class BaseHandler: log_message = "" operation_type = "" no_need_print = False def __init__(self, salt_client): self.salt_client = salt_client def _log(self, message, level="info"): message = f"\n{' ' * 30}".join(message.split("\n")) getattr(logger, level)(message) msg_str = f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S,%f')[:-3]} " \ f"{level.upper()} " \ f"{message}" # 适配命令行输出 print(msg_str) if not self.detail.message: self.detail.message = "" self.detail.message += f"{msg_str}\n" # todo: 优化下写库? self.detail.save() def success_handler(self): """成功处理""" self.detail.handler_info[self.__class__.__name__] = True if not (self.service.is_static and self.no_need_print): self._log( self.log_message.format(self.union_server, '成功!'), "info" ) self.write_db(True) return self.detail, self.service, self.relation_details def fail_handler(self): """失败处理""" self.detail.handler_info[self.__class__.__name__] = False self._log(self.log_message.format(self.union_server, '失败!'), "error") self.write_db(False) return None @classmethod def valid_args(cls, detail_args, detail): if not isinstance(detail_args, tuple) or len(detail_args) != 3: return False if not all([ isinstance(detail_args[0], detail), isinstance(detail_args[1], Service) ]): return False return True def __call__(self, operation_args, *args, **kwargs): if not self.valid_args(operation_args, self.detail_class): return None self.detail = operation_args[0] self.service = operation_args[1] self.relation_details = operation_args[2] # 执行成功部分跳过 if self.detail.handler_info.get(self.__class__.__name__): return self.detail, self.service, self.relation_details if not (self.service.is_static and self.no_need_print): self._log(self.log_message.format( self.union_server, '中...'), "info" ) try: success_state = self.handler() except Exception as e: self._log( f"服务实例:{self.union_server} " f"{self.operation_type.lower()}过程中出现异常:{str(e)}", "error" ) success_state = False if not success_state: return self.fail_handler() return self.success_handler() class StartOperationMixin: def handler(self): self.set_service_state() return self.detail, self.service, self.relation_details def __call__(self, operation_args, *args, **kwargs): if not self.valid_args(operation_args, self.detail_class): return None self.detail = operation_args[0] self.service = operation_args[1] self.relation_details = operation_args[2] self._log(self.log_message.format(self.union_server), "info") return self.handler() class StopServiceMixin: log_message = "服务实例{}: 停止服务{}" no_need_print = True def stop_service(self, service): for i in range(2): state, message = self.salt_client.cmd( self.service.ip, f'bash {service.service_controllers.get("stop")}', timeout=settings.SSH_CMD_TIMEOUT ) if not state: raise Exception(f"salt执行命令失败,错误输出: {str(message)}") if "[not running]" in message: # 休眠5秒等待停止 time.sleep(5) return True # 回滚有可能服务无包报错,也算停止 if "[running]" not in message: time.sleep(5) return True time.sleep(5) return False class StartServiceMixin: log_message = "服务实例{}: 启动服务{}" no_need_print = True def start_service(self, service): for i in range(2): state, message = self.salt_client.cmd( self.service.ip, service.service_controllers.get("start"), timeout=settings.SSH_CMD_TIMEOUT ) if not state: raise Exception(f"salt执行命令失败,错误输出: {str(message)}") if "[running]" in message: # 休眠5秒等待停止 time.sleep(10) return True time.sleep(5) return True def handler_pipeline(handlers, objs): """ 升级、回滚处理 :param handlers: handler类列表: upgrade_handlers, rollback_handlers :param objs: 操作对象(upgrade_detail, service) :return: """ salt_client = SaltClient() return reduce(lambda x, handler: handler(salt_client)(x), handlers, objs) ================================================ FILE: omp_server/service_upgrade/handler/rollback_handler.py ================================================ import logging import os import random import time from datetime import datetime from django.conf import settings from django.db import transaction from db_models.mixins import RollbackStateChoices from db_models.models import RollbackDetail, Service, ServiceHistory from service_upgrade.handler.base import BaseHandler, StartOperationMixin, \ StartServiceMixin logger = logging.getLogger(__name__) class RollbackBaseHandler(BaseHandler): operation_type = "ROLLBACK" @transaction.atomic def write_db(self, result=False): """写库""" if not result: # 升级失败,更新upgrade detail、service状态 state = f"{self.operation_type}_FAIL" self.service.service_status = Service.SERVICE_STATUS_UNKNOWN self.service.save() self.detail.rollback_state = getattr(RollbackStateChoices, state) ServiceHistory.create_history(self.service, self.detail) self.detail.save() # 升级结束更新服务表 self.sync_relation_details() def sync_relation_details(self): for relation_detail in self.relation_details: for k in {"path_info", "handler_info", "message", "rollback_state"}: v = getattr(self.detail, k) setattr(relation_detail, k, v) relation_detail.save() service = relation_detail.upgrade.service service.service_status = self.service.service_status service.save() ServiceHistory.create_history(service, relation_detail) relation_detail.refresh_from_db() def handler(self): raise NotImplementedError @property def detail_class(self): return RollbackDetail @property def union_server(self): return self.detail.upgrade.union_server class StartRollbackHandler(StartOperationMixin, RollbackBaseHandler): log_message = "服务实例{}: 开始回滚..." operation_type = "ROLLBACK" @transaction.atomic def set_service_state(self): self.detail.rollback_state = RollbackStateChoices.ROLLBACK_ING self.detail.save() self.detail.refresh_from_db() self.service.service_status = Service.SERVICE_STATUS_ROLLBACK self.service.save() self.service.refresh_from_db() for detail in self.relation_details: detail.rollback_state = RollbackStateChoices.ROLLBACK_ING service = detail.upgrade.service service.service_status = Service.SERVICE_STATUS_ROLLBACK service.save() detail.save() detail.refresh_from_db() class RollBackStopServiceHandler(RollbackBaseHandler): operation_type = "ROLLBACK" log_message = "服务实例{}: 停止服务{}" no_need_print = True def stop_service(self, service): for i in range(2): state, message = self.salt_client.cmd( self.service.ip, f'bash {service.service_controllers.get("stop")}', timeout=settings.SSH_CMD_TIMEOUT ) if "[not running]" in message: # 休眠5秒等待停止 time.sleep(5) return True time.sleep(5) return True def handler(self): if self.service.is_static: self._log("服务无需停止,跳过!") return True success = self.stop_service(self.service) if not success: return False for relation_detail in self.relation_details: success = self.stop_service(relation_detail.upgrade.service) if not success: return False return True class RollBackServiceHandler(RollbackBaseHandler): log_message = "服务实例{}: 回滚{}" operation_type = "ROLLBACK" def handler(self): rollback_file = self.detail.upgrade.path_info.get('backup_file_path') if not rollback_file: return True data_json_path = os.path.join( self.detail.data_path, f"omp_packages/{self.service._uuid}.json" ) rollback_path = self.service.service_controllers.get("rollback") if not rollback_path: rollback_path = os.path.join( self.service.install_folder, "scripts/rollback.py" ) cmd_str = f"python {rollback_path} " \ f"--local_ip {self.service.ip} " \ f"--data_json {data_json_path} " \ f"--version {self.detail.upgrade.current_app.app_version} " \ f"--backup_path {rollback_file}" state, message = self.salt_client.cmd( self.service.ip, cmd_str, settings.SSH_CMD_TIMEOUT ) if not state: self._log(f"执行回滚命令失败:{cmd_str}!\n错误信息: {message}", "error") return False if "备份路径:" in message: self.detail.path_info.update( rollback_backup_path=message.split("备份路径:")[-1] ) return True class RollBackStartServiceHandler(StartServiceMixin, RollbackBaseHandler): operation_type = "ROLLBACK" def sync_relation_details(self): for relation_detail in self.relation_details: for k in {"path_info", "handler_info", "message", "rollback_state"}: v = getattr(self.detail, k) setattr(relation_detail, k, v) relation_detail.save() relation_detail.upgrade.has_rollback = True relation_detail.upgrade.save() service = relation_detail.upgrade.service service.update_application( self.detail.upgrade.current_app, True, self.service.install_folder ) ServiceHistory.create_history(service, relation_detail) relation_detail.refresh_from_db() def write_db(self, result=False): """写库""" state = f"{self.operation_type}_SUCCESS" self.detail.rollback_state = getattr(RollbackStateChoices, state) self.detail.upgrade.has_rollback = True self.detail.upgrade.save() self.detail.save() # 创建服务操作记录 ServiceHistory.create_history(self.service, self.detail) # 升级结束更新服务表 self.service.update_application( self.detail.upgrade.current_app, True, self.service.install_folder ) # 同步关联服务 self.sync_relation_details() def handler(self): if self.service.is_static: self._log("服务无需启动,跳过!") return True self.start_service(self.service) for relation_detail in self.relation_details: self.start_service(relation_detail.upgrade.service) return True """ 回滚流程: 1、停服务 2、调用回滚脚本(参数传目标主机ip、data.json、目标版本version、升级步骤2的包路径) 3、启动服务(回滚成功以步骤2的结果为准) """ rollback_handlers = [ StartRollbackHandler, RollBackStopServiceHandler, RollBackServiceHandler, RollBackStartServiceHandler ] ================================================ FILE: omp_server/service_upgrade/handler/upgrade_handler.py ================================================ import json import logging import os import random from datetime import datetime from django.conf import settings from django.db import transaction from db_models.mixins import UpgradeStateChoices from db_models.models import UpgradeDetail, Service, ServiceHistory from service_upgrade.handler.base import BaseHandler, StartOperationMixin, \ StopServiceMixin, StartServiceMixin logger = logging.getLogger(__name__) class UpgradeBaseHandler(BaseHandler): operation_type = "UPGRADE" @transaction.atomic def write_db(self, result=False): """写库""" if not result: # 升级失败,更新upgrade detail、service状态 state = f"{self.operation_type}_FAIL" self.service.service_status = Service.SERVICE_STATUS_UNKNOWN self.service.save() ServiceHistory.create_history(self.service, self.detail) self.detail.upgrade_state = getattr(UpgradeStateChoices, state) self.detail.save() # 升级结束更新服务表 self.sync_relation_details() def sync_relation_details(self): for relation_detail in self.relation_details: for k in {"path_info", "handler_info", "message", "upgrade_state"}: v = getattr(self.detail, k) setattr(relation_detail, k, v) relation_detail.save() service = relation_detail.service service.service_status = self.service.service_status service.save() ServiceHistory.create_history(service, relation_detail) relation_detail.refresh_from_db() def handler(self): raise NotImplementedError @property def detail_class(self): return UpgradeDetail @property def union_server(self): return self.detail.union_server class StartUpgradeHandler(StartOperationMixin, UpgradeBaseHandler): log_message = "服务实例{}: 开始升级..." @transaction.atomic def set_service_state(self): self.detail.upgrade_state = UpgradeStateChoices.UPGRADE_ING self.detail.save() self.detail.refresh_from_db() self.service.service_status = Service.SERVICE_STATUS_UPGRADE self.service.save() self.service.refresh_from_db() for detail in self.relation_details: detail.upgrade_state = UpgradeStateChoices.UPGRADE_ING detail.service.service_status = \ Service.SERVICE_STATUS_UPGRADE detail.service.save() detail.save() detail.refresh_from_db() class StopServiceHandler(StopServiceMixin, UpgradeBaseHandler): def handler(self): if self.service.is_static: self._log("服务无需停止,跳过!") return True success = self.stop_service(self.service) if not success: return False for relation_detail in self.relation_details: success = self.stop_service(relation_detail.service) if not success: return False return True class BackupServiceHandler(UpgradeBaseHandler): log_message = "服务实例{}: 备份服务包{}" def do_backup(self, backup_package): # 生成备份文件名 time_str = datetime.now().strftime("%Y%m%d%H%M") backup_name = f"{self.service.service.app_name}.back-{time_str}-" \ f"{random.randint(1, 120)}" # 备份服务文件 backup_file = os.path.join(backup_package, backup_name) backup_str = f"mkdir -p {backup_package} && " \ f"mv {self.service.install_folder} {backup_file}" state, result = self.salt_client.cmd( self.service.ip, backup_str, settings.SSH_CMD_TIMEOUT) if not state: raise Exception("备份原服务文件失败!\n 错误信息:{}".format(result)) self.detail.path_info.update( { "old_file_path": self.service.install_folder, "backup_file_path": backup_file, } ) return state def handler(self): backup_package = os.path.join( os.path.dirname(self.service.install_folder), f"upgrade_backup/{datetime.today().strftime('%Y%m%d')}" ) return self.do_backup(backup_package) class SendPackageHandler(UpgradeBaseHandler): log_message = "服务实例{}: 发送升级包{}" def handler(self): tar_package = os.path.join( self.detail.target_app.app_package.package_path, self.detail.target_app.app_package.package_name ) if not tar_package: raise Exception(f"升级包{tar_package}不存在") send_packages = os.path.join( self.detail.data_path, f"omp_packages" ) state, message = self.salt_client.cp_file( self.service.ip, tar_package, send_packages ) if not state: raise Exception("发送升级包失败!\n错误信息: {}".format(message)) return True class UnzipPackageHandler(UpgradeBaseHandler): log_message = "服务实例{}: 解压升级包{}" def handler(self): tar_packages_path = os.path.join( self.detail.data_path, "omp_packages" ) package_name = self.detail.target_app.app_package.package_name install_path = os.path.dirname(self.service.install_folder) cmd_str = f"cd {tar_packages_path} &&" \ f"tar -xmf {package_name} -C {install_path}" state, message = self.salt_client.cmd( self.service.ip, cmd_str, settings.SSH_CMD_TIMEOUT) if not state: raise Exception(f"解压升级包失败!\n错误信息: {message}") return state class UpgradeServiceHandler(UpgradeBaseHandler): log_message = "服务实例{}: 升级{}" def handler(self): upgrade_file = json.loads( self.detail.target_app.app_controllers ).get("upgrade", "scripts/upgrade.py") upgrade_path = os.path.join( self.service.install_folder, upgrade_file ) data_json_path = os.path.join( self.detail.data_path, f"omp_packages/{self.service._uuid}.json" ) # 适配流水线加的统一版本后缀commit_id version = self.detail.current_app.app_version.split("-")[0] cmd_str = f"python {upgrade_path} " \ f"--local_ip {self.service.ip} " \ f"--data_json {data_json_path} " \ f"--version {version} " \ f"--backup_path " \ f"{self.detail.path_info.get('backup_file_path')}" state, message = self.salt_client.cmd( self.service.ip, cmd_str, settings.SSH_CMD_TIMEOUT) if not state: raise Exception(f"通过salt执行命令:{cmd_str};\n错误输出:{message}!") return state class StartServiceHandler(StartServiceMixin, UpgradeBaseHandler): def sync_relation_details(self): for relation_detail in self.relation_details: for k in {"path_info", "handler_info", "message", "upgrade_state"}: v = getattr(self.detail, k) setattr(relation_detail, k, v) relation_detail.save() service = relation_detail.service service.update_application( self.detail.target_app, True, self.service.install_folder ) relation_detail.refresh_from_db() def write_db(self, result=False): """写库""" state = f"{self.operation_type}_SUCCESS" self.detail.upgrade_state = getattr(UpgradeStateChoices, state) self.detail.save() self.service.update_application( self.detail.target_app, True, self.service.install_folder ) # 升级结束更新服务表 self.sync_relation_details() def fail_handler(self): """失败处理""" self.detail.handler_info[self.__class__.__name__] = False self._log(self.log_message.format(self.union_server, '失败!'), "error") self.write_db(False) return self.detail, self.service, self.relation_details def handler(self): if self.service.is_static: self._log("服务无需启动,跳过!") return True self.start_service(self.service) for relation_detail in self.relation_details: self.start_service(relation_detail.service) return True """ 升级流程: 1、停服务 2、备份服务安装目录 3、发送服务升级包 4、解压升级包 5、调用服务升级流程(参数传目标主机ip、data.json、前一版本version、步骤2的包路径) 6、启动服务(升级成功以步骤5结果为准) """ upgrade_handlers = [ StartUpgradeHandler, StopServiceHandler, BackupServiceHandler, SendPackageHandler, UnzipPackageHandler, UpgradeServiceHandler, StartServiceHandler ] ================================================ FILE: omp_server/service_upgrade/serializers.py ================================================ from rest_framework import serializers from rest_framework.exceptions import ValidationError from db_models.mixins import UpgradeStateChoices, RollbackStateChoices from db_models.models import UpgradeHistory, UpgradeDetail, Service, \ ApplicationHub, RollbackHistory, RollbackDetail from utils.parse_config import BASIC_ORDER class UpgradeHistorySerializer(serializers.ModelSerializer): service_count = serializers.SerializerMethodField() operator = serializers.CharField(source="operator.username") state_display = serializers.CharField(source="get_upgrade_state_display") can_rollback = serializers.BooleanField(source="can_roll_back") def get_service_count(self, obj): return obj.upgradedetail_set.all().count() class Meta: model = UpgradeHistory fields = ("id", "operator", "service_count", "upgrade_state", "created", "can_rollback", "state_display") class RollbackHistorySerializer(serializers.ModelSerializer): service_count = serializers.SerializerMethodField() operator = serializers.CharField(source="operator.username") state_display = serializers.CharField(source="get_rollback_state_display") def get_service_count(self, obj): return obj.rollbackdetail_set.all().count() class Meta: model = RollbackHistory fields = ("id", "operator", "service_count", "rollback_state", "created", "state_display") class UpgradeDetailSerializer(serializers.ModelSerializer): ip = serializers.CharField(source="service.ip") service_name = serializers.CharField(source="service.service.app_name") state_display = serializers.CharField(source="get_upgrade_state_display") instance_name = serializers.CharField(source="service.service_instance_name") class Meta: model = UpgradeDetail fields = ("id", "ip", "service_name", "upgrade_state", "message", "state_display", "instance_name") class RollbackDetailSerializer(serializers.ModelSerializer): ip = serializers.CharField(source="upgrade.service.ip") service_name = serializers.CharField(source="upgrade.target_app.app_name") state_display = serializers.CharField(source="get_rollback_state_display") instance_name = serializers.CharField( source="upgrade.service.service_instance_name") class Meta: model = RollbackDetail fields = ("id", "ip", "service_name", "rollback_state", "message", "state_display", "instance_name") class UpgradeHistoryDetailSerializer(serializers.ModelSerializer): operator = serializers.CharField(source="operator.username") upgrade_detail = serializers.SerializerMethodField() can_rollback = serializers.SerializerMethodField() success_count = serializers.SerializerMethodField() all_count = serializers.SerializerMethodField() def get_upgrade_details(self, obj, key): if hasattr(obj, key): return getattr(obj, key) details = obj.upgradedetail_set.filter( service__isnull=False ) upgrade_details = UpgradeDetailSerializer(details, many=True).data # 合并服务 upgrade_result = {} success_count = all_count = 0 for detail in upgrade_details: all_count += 1 if detail.get("upgrade_state") ==\ UpgradeStateChoices.UPGRADE_SUCCESS: success_count += 1 if detail.get("service_name") not in upgrade_result: upgrade_result[detail.get("service_name")] = [detail] else: upgrade_result[detail.get("service_name")].append(detail) setattr(obj, "success_count", success_count) setattr(obj, "all_count", all_count) setattr(obj, "upgrade_result", upgrade_result) return getattr(obj, key) def get_pre_upgrade(self, obj): result = [] if not obj.pre_upgrade_result: pre_upgrade_result = {"update_data_json": {}} else: pre_upgrade_result = obj.pre_upgrade_result for k, v in pre_upgrade_result.items(): default_dict = { "id": 0, "ip": "", "service_name": k, "instance_name": k, "upgrade_state": v.get("state", 1), "message": v.get("message", ""), "state_display": v.get("state_display", "正在升级") } result.append(default_dict) return "升级前置操作", result def get_upgrade_detail(self, obj): result = self.get_upgrade_details(obj, "upgrade_result") # 获取服务顺序 service_index = {} sum_index = 0 for index, services in BASIC_ORDER.items(): for sub_index, service in enumerate(services, sum_index): service_index[service] = sub_index sum_index += 1 # 升级记录排序 results = sorted( result.items(), key=lambda x: service_index.get(x[0], sum_index+1) ) pre_upgrade = self.get_pre_upgrade(obj) results.insert(0, pre_upgrade) # 调整返回数据结构 return [ { "service_name": service_tmp[0], "upgrade_details": service_tmp[1] } for service_tmp in results ] def get_can_rollback(self, obj): return obj.can_roll_back def get_success_count(self, obj): return self.get_upgrade_details(obj, "success_count") def get_all_count(self, obj): return self.get_upgrade_details(obj, "all_count") class Meta: model = UpgradeHistory fields = ("id", "operator", "upgrade_detail", "upgrade_state", "created", "can_rollback", "success_count", "all_count") class RollbackHistoryDetailSerializer(serializers.ModelSerializer): operator = serializers.CharField(source="operator.username") rollback_detail = serializers.SerializerMethodField() success_count = serializers.SerializerMethodField() all_count = serializers.SerializerMethodField() def get_rollback_details(self, obj, key): if hasattr(obj, key): return getattr(obj, key) details = obj.rollbackdetail_set.filter( upgrade__service__isnull=False ) rollback_details = RollbackDetailSerializer(details, many=True).data # 合并服务 rollback_result = {} success_count = all_count = 0 for detail in rollback_details: all_count += 1 if detail.get("rollback_state") ==\ RollbackStateChoices.ROLLBACK_SUCCESS: success_count += 1 if detail.get("service_name") not in rollback_result: rollback_result[detail.get("service_name")] = [detail] else: rollback_result[detail.get("service_name")].append(detail) setattr(obj, "success_count", success_count) setattr(obj, "all_count", all_count) setattr(obj, "rollback_result", rollback_result) return getattr(obj, key) def get_rollback_detail(self, obj): result = self.get_rollback_details(obj, "rollback_result") # 获取服务顺序 service_index = {} sum_index = 0 for index, services in BASIC_ORDER.items(): for sub_index, service in enumerate(services, sum_index): service_index[service] = sub_index sum_index += 1 # 升级记录排序 results = sorted( result.items(), key=lambda x: service_index.get(x[0], sum_index+1) ) # 调整返回数据结构 return [ { "service_name": service_tmp[0], "rollback_details": service_tmp[1] } for service_tmp in results ] def get_success_count(self, obj): return self.get_rollback_details(obj, "success_count") def get_all_count(self, obj): return self.get_rollback_details(obj, "all_count") class Meta: model = RollbackHistory fields = ("id", "operator", "rollback_detail", "rollback_state", "created", "success_count", "all_count") class UpgradeTryAgainSerializer(serializers.ModelSerializer): id = serializers.IntegerField(read_only=True) class Meta: model = UpgradeHistory fields = ("id", ) def validate(self, attrs): if self.instance.upgrade_state != UpgradeStateChoices.UPGRADE_FAIL: raise ValidationError("该升级记录不支持重新再次升级!") histories = UpgradeHistory.objects.filter( upgrade_state__in=[ UpgradeStateChoices.UPGRADE_WAIT, UpgradeStateChoices.UPGRADE_ING ] ) if histories.exists(): raise ValidationError("存在正在升级的服务,请稍后重试!") if UpgradeHistory.objects.filter(id__gt=self.instance.id).exists(): raise ValidationError("历史记录不可再次升级!") return True class RollbackTryAgainSerializer(serializers.ModelSerializer): id = serializers.IntegerField(read_only=True) class Meta: model = RollbackHistory fields = ("id", ) def validate(self, attrs): if self.instance.rollback_state != RollbackStateChoices.ROLLBACK_FAIL: raise ValidationError("该回滚记录不支持重新再次回滚!") if RollbackHistory.objects.filter(id__gt=self.instance.id).exists(): raise ValidationError("历史记录不可再次回滚!") return True class ApplicationHubSerializer(serializers.ModelSerializer): app_id = serializers.IntegerField(source="id") class Meta: model = ApplicationHub fields = ("app_id", "app_name", "app_version") class ServiceSerializer(serializers.ModelSerializer): service_id = serializers.IntegerField(source="id") instance_name = serializers.CharField(source="service_instance_name") app_name = serializers.CharField(source="service.app_name") app_id = serializers.IntegerField(source="service.id") version = serializers.CharField(source="service.app_version") class Meta: model = Service fields = ("service_id", "ip", "instance_name", "app_name", "version", "app_id") class RollbackListSerializer(serializers.ModelSerializer): before_rollback_v = serializers.CharField(source="target_app.app_version") after_rollback_v = serializers.CharField(source="current_app.app_version") app_name = serializers.CharField(source="target_app.app_name") ip = serializers.CharField(source="service.ip") instance_name = serializers.CharField(source="service.service_instance_name") class Meta: model = UpgradeDetail fields = ("id", "ip", "service_id", "app_name", "before_rollback_v", "after_rollback_v", "instance_name") ================================================ FILE: omp_server/service_upgrade/tasks.py ================================================ import logging import time from concurrent.futures import ThreadPoolExecutor, as_completed, wait, \ ALL_COMPLETED from celery import shared_task from db_models.mixins import UpgradeStateChoices, RollbackStateChoices from db_models.models import UpgradeHistory, RollbackHistory, \ RollbackDetail, Maintain, MainInstallHistory, Product, ApplicationHub from promemonitor.alertmanager import Alertmanager from service_upgrade.handler.base import load_upgrade_detail, \ handler_pipeline, load_rollback_detail from service_upgrade.handler.rollback_handler import rollback_handlers from service_upgrade.handler.upgrade_handler import upgrade_handlers from service_upgrade.update_data_json import DataJsonUpdate from utils.parse_config import BASIC_ORDER, THREAD_POOL_MAX_WORKERS logger = logging.getLogger(__name__) def get_service_order(): # 获取配置文件层次,解析成dict service_layer = {} for index, services in BASIC_ORDER.items(): for service in services: service_layer[service] = index return service_layer def computer_operation_sorted(details): # 通过服务依赖确定服务升级、回滚顺序 order_layer_details = {} service_layer = get_service_order() max_index = max(service_layer.values()) + 1 union_service = set() for detail in details: if isinstance(detail, RollbackDetail): union_server = detail.upgrade.union_server app_name = detail.upgrade.current_app.app_name extend_fields = detail.upgrade.current_app.extend_fields else: union_server = detail.union_server app_name = detail.current_app.app_name extend_fields = detail.target_app.extend_fields if union_server in union_service: continue union_service.add(union_server) s_i = service_layer.get(app_name, None) if s_i is None: s_i = max_index + int(extend_fields.get("level", 0)) if s_i not in order_layer_details: order_layer_details[s_i] = [detail] else: order_layer_details[s_i].append(detail) # [(0, [detail1]), (1, [detail2, detail3])] return sorted(order_layer_details.items(), key=lambda x: x[0]) def set_alert_maintain(env_name): try: has_maintain = Maintain.objects.filter( matcher_name="env", matcher_value=env_name ).exists() if not has_maintain: return Alertmanager().set_maintain_by_env_name(env_name) except Exception as e: logger.error(f"进入维护模式失败:{str(e)}") return None def update_data_json(operation_uuid, details): data_json = DataJsonUpdate(operation_uuid) try: data_json.create_json_file(details) fail_message = data_json.send_data_json_all() except Exception as e: logger.error(str(e)) return False, str(e) if fail_message: return False, ",".join(fail_message) return True, "data.json更新成功!" def load_upgrade_service(history): app_ids = list( history.upgradedetail_set.filter( service__service__product__isnull=False ).values_list("service__service_id", flat=True) ) return app_ids def load_rollback_service(history): app_ids = list( history.rollbackdetail_set.filter( upgrade__service__service__product__isnull=False ).values_list("upgrade__service__service_id", flat=True) ) return app_ids def update_product_version(app_ids): product_info = ApplicationHub.objects.filter( id__in=app_ids ).values("product__pro_name", "product_id") product_union = {} for product in product_info: product_name = product["product__pro_name"] product_id = product["product_id"] if product_name not in product_union: product_union[product_name] = {product_id} else: product_union[product_name].add(product_id) for product_name, product_ids in product_union.items(): if len(product_ids) > 1: continue Product.objects.filter( product_instance_name__startswith=product_name ).update(product_id=product_ids.pop()) @shared_task def upgrade_service(upgrade_history_id): history = UpgradeHistory.objects.filter(id=upgrade_history_id).first() if not history: logger.error(f"未找到id为{upgrade_history_id}的升级操作!") return if history.upgrade_state not in { UpgradeStateChoices.UPGRADE_WAIT, UpgradeStateChoices.UPGRADE_FAIL }: logger.error(f"升级记录状态为{history.get_upgrade_state_display()}," f"不可升级!") return main_install = MainInstallHistory.objects.order_by("-id").first() upgrade_details = history.upgradedetail_set.exclude( upgrade_state=UpgradeStateChoices.UPGRADE_SUCCESS ).exclude(has_rollback=True) if history.pre_upgrade_state != UpgradeStateChoices.UPGRADE_SUCCESS: state, msg = update_data_json( main_install.operation_uuid, upgrade_details) # todo:后续优化 history.pre_upgrade_result = { "update_data_json": { "state": 2 if state else 3, "message": msg, "state_display": "升级成功" if state else "升级失败" } } if not state: history.pre_upgrade_state = UpgradeStateChoices.UPGRADE_FAIL history.upgrade_state = UpgradeStateChoices.UPGRADE_FAIL history.save() return else: history.pre_upgrade_state = UpgradeStateChoices.UPGRADE_SUCCESS history.save() if history.upgrade_state != UpgradeStateChoices.UPGRADE_ING: history.upgrade_state = UpgradeStateChoices.UPGRADE_ING history.save() # 排除hadoop等多余服务,升级只升一次 order_layer_details = computer_operation_sorted(upgrade_details) # 进入维护模式 set_alert_maintain(history.env.name) with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: upgrade_state = UpgradeStateChoices.UPGRADE_SUCCESS for sort, details in order_layer_details: all_task = [] for detail in details: upgrade_args = load_upgrade_detail( detail, main_install.operation_uuid) future_obj = executor.submit( handler_pipeline, upgrade_handlers, upgrade_args) all_task.append(future_obj) wait(all_task, return_when=ALL_COMPLETED) for future in as_completed(all_task): upgrade_args = future.result() if upgrade_args is None: upgrade_state = UpgradeStateChoices.UPGRADE_FAIL break if upgrade_state == UpgradeStateChoices.UPGRADE_FAIL: break time.sleep(5) history.upgrade_state = upgrade_state history.save() app_ids = load_upgrade_service(history) update_product_version(app_ids) @shared_task def rollback_service(rollback_history_id): history = RollbackHistory.objects.filter(id=rollback_history_id).first() if not history: logger.error(f"未找到id为{rollback_history_id}的回滚操作!") return if history.rollback_state not in { RollbackStateChoices.ROLLBACK_WAIT, RollbackStateChoices.ROLLBACK_FAIL }: logger.error(f"回滚记录状态为{history.get_rollback_state_display()},不可回滚!") return if history.rollback_state != RollbackStateChoices.ROLLBACK_ING: history.rollback_state = RollbackStateChoices.ROLLBACK_ING history.save() # 排除hadoop等多余服务,升级只升一次 rollback_details = history.rollbackdetail_set.exclude( rollback_state=RollbackStateChoices.ROLLBACK_SUCCESS ) order_layer_details = computer_operation_sorted(rollback_details) set_alert_maintain(history.env.name) main_install = MainInstallHistory.objects.order_by("-id").first() with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: rollback_state = RollbackStateChoices.ROLLBACK_SUCCESS for sort, details in order_layer_details: all_task = [] for detail in details: rollback_args = load_rollback_detail( detail, main_install.operation_uuid) future_obj = executor.submit( handler_pipeline, rollback_handlers, rollback_args) all_task.append(future_obj) wait(all_task, return_when=ALL_COMPLETED) for future in as_completed(all_task): rollback_args = future.result() if rollback_args is None: rollback_state = RollbackStateChoices.ROLLBACK_FAIL break if rollback_state == RollbackStateChoices.ROLLBACK_FAIL: break history.rollback_state = rollback_state history.save() app_ids = load_rollback_service(history) update_product_version(app_ids) ================================================ FILE: omp_server/service_upgrade/update_data_json.py ================================================ import json import os from django.conf import settings from db_models.models import DetailInstallHistory, Service, Host, UpgradeDetail from utils.plugin.salt_client import SaltClient class DataJsonUpdate(object): """ 生成data.json数据 """ def __init__(self, operation_uuid): """ data.json数据生成方法 :param operation_uuid: 唯一操作uuid :type operation_uuid: str """ self.json_name = f"{operation_uuid}.json" self.data_path = os.path.join("data_files", self.json_name) def get_ser_install_args(self, obj, app_install_args=None): """ 获取服务的安装参数 :param obj: Service obj :param app_install_args: app_install_args, list :return: """ deploy_detail = DetailInstallHistory.objects.filter( service=obj).first() install_args = [] deploy_mode = "" if deploy_detail: install_args = \ deploy_detail.install_detail_args.get("install_args", []) deploy_mode = \ deploy_detail.install_detail_args.get("deploy_mode") old_arg_dict = {} for old_arg in install_args: old_arg_dict[old_arg["key"]] = old_arg if app_install_args: for new_arg in app_install_args: if new_arg.get("key") not in old_arg_dict: new_arg["default"] = new_arg["default"].format( data_path="data_path") install_args.append(new_arg) if deploy_detail: deploy_detail.install_detail_args["install_args"] = install_args deploy_detail.save() return { "install_args": install_args, "deploy_mode": deploy_mode } def parse_single_service(self, service, tag_app=None): """ 解析单个服务数据 :param server: Service :param tag_app: Service :return: """ _ser_dic = { "ip": service.ip, "name": service.service.app_name, "role": service.service_role if service.service_role else "master", "instance_name": service.service_instance_name, "cluster_name": service.cluster.cluster_name if service.cluster else None, "vip": service.vip, "ports": json.loads(service.service_port or '[]'), "dependence": json.loads(service.service_dependence or '[]'), } if service.service.app_name == "hadoop": _ser_dic["instance_name"] = \ "hadoop-" + "-".join(service.ip.split(".")[-2:]) if tag_app: _ser_dic["ports"] = service.update_port( json.loads(tag_app.app_port or '[]') ) _ser_dic["dependence"] = service.update_dependence( service.service_dependence, json.loads(tag_app.app_dependence or '[]') ) _others = self.get_ser_install_args( service, json.loads(tag_app.app_install_args or '[]')) else: _others = self.get_ser_install_args(service) _ser_dic.update(_others) return _ser_dic def make_data_json(self, json_lst): """ 创建data.json数据文件 :param json_lst: 服务及分布信息组成的列表 :type json_lst: list :return: """ _path = os.path.join( settings.PROJECT_DIR, "package_hub", self.data_path ) if not os.path.exists(os.path.dirname(_path)): os.makedirs(os.path.dirname(_path)) with open(_path, "w", encoding="utf8") as fp: json.dump(json_lst, fp, ensure_ascii=False, indent=2) def decompose_detail(self, details): _dic = {} if not details: return _dic for detail in details: if isinstance(detail, UpgradeDetail): _dic[detail.service.service_instance_name] = detail.target_app else: _dic[detail.upgrade.service.service_instance_name] = \ detail.current_app return _dic def load_json_lst(self, details): # 在json文件中标记该服务所在主机上的agent的地址 ip_agent_dir_dir = { ip: agent_dir for ip, agent_dir in Host.objects.values_list("ip", "agent_dir") } json_lst = list() services = Service.split_objects.all() for service in services: tag_app = details.get(service.service_instance_name) _item = self.parse_single_service(service, tag_app) _item["agent_dir"] = ip_agent_dir_dir.get(_item.get("ip")) json_lst.append(_item) return json_lst def send_data_json_target(self, salt_obj, target_ip): host = Host.objects.get(ip=target_ip) json_target_path = os.path.join( host.data_folder, "omp_packages", self.json_name) return salt_obj.cp_file( target=target_ip, source_path=self.data_path, target_path=json_target_path ) def send_data_json_all(self): hosts = Host.objects.all().values_list("ip", "data_folder") fail_message = [] salt_obj = SaltClient() for host in hosts: json_target_path = os.path.join( host[1], "omp_packages", self.json_name) state, message = salt_obj.cp_file( target=host[0], source_path=self.data_path, target_path=json_target_path ) if not state: fail_message.append( f"ip:{host[1]}更新data.json失败,错误:{message}") return fail_message def create_json_file(self, details=None): """ 更新data.json :param details: 升级details :return: """ details = self.decompose_detail(details) current_json_lst = self.load_json_lst(details) # step2: 生成data.json self.make_data_json(json_lst=current_json_lst) ================================================ FILE: omp_server/service_upgrade/urls.py ================================================ from django.urls import path from service_upgrade.views import UpgradeChoiceMaxVersionListAPIView, \ UpgradeHistoryListAPIView, UpgradeHistoryDetailAPIView, DoUpgradeAPIView, \ RollbackHistoryListAPIView, RollbackHistoryDetailAPIView, \ RollbackChoiceListAPIView, DoRollbackAPIView upgrade_urlpatterns = [ path('history', UpgradeHistoryListAPIView.as_view(), name="history"), path( 'history/', UpgradeHistoryDetailAPIView.as_view(), name="detail"), path( 'can-upgrade', UpgradeChoiceMaxVersionListAPIView.as_view(), name="can-upgrade"), path('do-upgrade', DoUpgradeAPIView.as_view(), name="do-upgrade") ] rollback_urlpatterns = [ path('history', RollbackHistoryListAPIView.as_view(), name="history"), path( 'history/', RollbackHistoryDetailAPIView.as_view(), name="detail"), path( 'can-rollback', RollbackChoiceListAPIView.as_view(), name="can-rollback"), path('do-rollback', DoRollbackAPIView.as_view(), name="do-rollback") ] ================================================ FILE: omp_server/service_upgrade/views.py ================================================ import json import logging import traceback from django.db import models, transaction from rest_framework.filters import OrderingFilter, SearchFilter from rest_framework.generics import ListAPIView, GenericAPIView,\ RetrieveUpdateAPIView from rest_framework.response import Response from db_models.mixins import UpgradeStateChoices, RollbackStateChoices from db_models.models import UpgradeHistory, Service, ApplicationHub, Env, \ UpgradeDetail, RollbackHistory, RollbackDetail from utils.common.exceptions import GeneralError from utils.common.paginations import PageNumberPager from .filters import RollBackHistoryFilter from .serializers import UpgradeHistorySerializer, ServiceSerializer, \ UpgradeHistoryDetailSerializer, ApplicationHubSerializer, \ UpgradeTryAgainSerializer, RollbackHistorySerializer, \ RollbackHistoryDetailSerializer, RollbackTryAgainSerializer, \ RollbackListSerializer from .tasks import upgrade_service, rollback_service logger = logging.getLogger("server") class UpgradeHistoryListAPIView(ListAPIView): # 升级历史记录 pagination_class = PageNumberPager queryset = UpgradeHistory.objects.all()\ .prefetch_related("upgradedetail_set") filter_backends = (OrderingFilter, ) serializer_class = UpgradeHistorySerializer ordering_fields = ("id", ) ordering = ('-id',) get_description = "升级历史记录页" class UpgradeHistoryDetailAPIView(RetrieveUpdateAPIView): # 升级历史记录详情 queryset = UpgradeHistory.objects.all()\ .prefetch_related("upgradedetail_set") serializer_class = UpgradeHistoryDetailSerializer lookup_url_kwarg = 'pk' get_description = "升级历史记录详情页" put_description = "升级重试" def update(self, request, *args, **kwargs): # put 升级重试 instance = self.get_object() serializer = UpgradeTryAgainSerializer(instance, data=request.data) serializer.is_valid(raise_exception=True) upgrade_service.delay(instance.id) return Response() class UpgradeChoiceAllVersionListAPIView(GenericAPIView): # 可升级服务列表(可选择升级的目标) queryset = Service.split_objects.filter( service_status__in=[0, 1, 2, 3, 4] ).select_related("service") filter_backends = (SearchFilter, ) search_fields = ("service__app_name",) get_description = "可升级服务列表" def get_service_info(self, services_data): """ 确定已安装服务最小版本,缩小查询范围及组装数据 """ service_min_app_ids, services_versions = {}, {} for service_data in services_data: app_name = service_data.get("app_name") app_id = service_data.get("app_id") min_app_id = service_min_app_ids.get(app_name, None) if app_name not in services_versions: services_versions[app_name] = [service_data] else: services_versions[app_name].append(service_data) if min_app_id is None or app_id < min_app_id: service_min_app_ids[app_name] = app_id return services_versions, service_min_app_ids def get(self, requests): queryset = self.filter_queryset(self.get_queryset()) services_data = ServiceSerializer(queryset, many=True).data if not services_data: return Response({"results": []}) services_v, min_app_ids = self.get_service_info( services_data) # 查询服务可能存在可升级的安装包 apps = ApplicationHub.objects.filter( id__gt=min(min_app_ids.values()), app_name__in=services_v.keys() ).order_by("-id").exclude(id__in=min_app_ids.values()) if not apps: return Response({"results": []}) # 确定服务可升级的安装包 apps_data = ApplicationHubSerializer(apps, many=True).data for app_data in apps_data: app_id = app_data.get("app_id") app_name = app_data.pop("app_name") if min_app_ids.get(app_name, float("inf")) >= app_id: apps_data.remove(app_data) continue for service_v in services_v.get(app_name): if service_v.get("app_id", float("inf")) >= app_id: continue if "can_upgrade" not in service_v: service_v["can_upgrade"] = [app_data] else: service_v["can_upgrade"].append(app_data) # 格式化数据,排除不可升级服务 results = [] # {"a": [{""}, {}]} for app_name, services in services_v.items(): if not services: services_v.pop(app_name) upgrade_services = [] for service in services: if service.get("can_upgrade"): upgrade_services.append(service) if upgrade_services: results.append( {"app_name": app_name, "children": upgrade_services} ) # results: [{"app_name": a, "service": [{"can_upgrade": []...}] return Response({"results": results}) class UpgradeChoiceMaxVersionListAPIView(UpgradeChoiceAllVersionListAPIView): # 可升级服务列表(只展示可供升级的最高版本) get_description = "可升级服务列表" def get_service_max_app(self, apps): max_apps = {} for app in apps: app_info = max_apps.get(app["app_name"], {}) if app_info.get("app_id", float("-inf")) <= app["app_id"]: max_apps[app["app_name"]] = app return max_apps def get(self, requests): queryset = self.filter_queryset(self.get_queryset()) services_data = ServiceSerializer(queryset, many=True).data if not services_data: return Response({"results": []}) services_v, min_app_ids = self.get_service_info( services_data) # 查询服务可能存在可升级的安装包 apps = ApplicationHub.objects.filter( id__gt=min(min_app_ids.values()), app_name__in=services_v.keys() ).order_by("-id").exclude(id__in=min_app_ids.values()) if not apps: return Response({"results": []}) # 确定服务可升级的安装包 apps_data = ApplicationHubSerializer(apps, many=True).data max_apps_dict = self.get_service_max_app(apps_data) results = [] for app_name, max_app in max_apps_dict.items(): services = services_v.get(app_name) upgrade_services = [] for service_v in services: if service_v.get("app_id", float("inf")) >= max_app["app_id"]: continue service_v["can_upgrade"] = [max_app] upgrade_services.append(service_v) if upgrade_services: results.append( { "app_name": app_name, "children": upgrade_services, "can_upgrade": max_app } ) return Response({"results": results}) class DoUpgradeAPIView(GenericAPIView): post_description = "升级服务" def valid_can_upgrade(self, data): # 校验信息 services = list( Service.split_objects.filter( id__in=data.keys(), service_status__in=[0, 1, 2, 3, 4] ).annotate( app_name=models.F("service__app_name"), current_app_id=models.F("service_id") ).values( "id", "app_name", "ip", "current_app_id", "service_dependence" ) ) if not services: raise GeneralError("请选择需要升级的服务!") apps = ApplicationHub.objects.filter( id__in=data.values(), is_release=True ).values("id", "app_name", "app_version", "app_dependence") app_dict = {} for app in apps: # todo: app_dependence字段有问题,后续需修改 app_dict[app.get("app_name")] = { "target_app_id": app.get("id"), "app_dependence": json.loads( app.get("app_dependence") or '[]' ) } for service in services: app_name = service.get("app_name") app_info = app_dict.get(app_name, {}) if app_info.get("target_app_id", float("-inf")) \ <= service["current_app_id"]: raise GeneralError(f"服务{app_name}升级版本小于或等于当前版本!") try: Service.update_dependence( service.get("service_dependence"), app_info.get("app_dependence", []) ) except Exception as e: raise GeneralError( f"服务{service.get('app_name')}依赖校验失败:{str(e)}") service.update(app_dict.get(app_name)) return services def post(self, requests): if UpgradeHistory.objects.filter( upgrade_state__in=[ UpgradeStateChoices.UPGRADE_WAIT, UpgradeStateChoices.UPGRADE_ING, ] ).exists(): raise GeneralError("存在正在升级的服务,请稍后!") if RollbackHistory.objects.filter( rollback_state__in=[ RollbackStateChoices.ROLLBACK_WAIT, RollbackStateChoices.ROLLBACK_ING, ] ).exists(): raise GeneralError("存在正在回滚的服务,请稍后!") fail_query = UpgradeDetail.objects.filter( upgrade_state=UpgradeStateChoices.UPGRADE_FAIL, service__isnull=False ).exclude(has_rollback=True) if fail_query.exists(): fail_services = list( fail_query.values_list("union_server", flat=True) ) raise GeneralError( f"存在升级失败的服务,请继续升级或回滚!失败服务:{fail_services}") choices = requests.data.get("choices", []) if not choices: raise GeneralError("请选择需要升级的服务!") try: data = {} for choice in choices: if choice.get("service_id") in data: raise KeyError(f'{choice.get("service_id")}重复!') data[choice.get("service_id")] = choice.get("app_id") except Exception as e: logger.error( f"解析升级数据错误:{str(e)}, 详情为:\n{traceback.format_exc()}") raise GeneralError("解析升级数据错误!") services = self.valid_can_upgrade(data) with transaction.atomic(): history = UpgradeHistory.objects.create( env=Env.objects.first(), operator=requests.user ) details = [] for service in services: service_id = service.pop("id") app_name = service.pop("app_name") ip = service.pop("ip") service.pop("app_dependence") service.pop("service_dependence") details.append( UpgradeDetail( history=history, service_id=service_id, union_server=f"{ip}-{app_name}", **service, ) ) UpgradeDetail.objects.bulk_create(details) upgrade_service.delay(history.id) return Response({"history": history.id}) class RollbackHistoryListAPIView(ListAPIView): pagination_class = PageNumberPager queryset = RollbackHistory.objects.all()\ .prefetch_related("rollbackdetail_set") filter_backends = (OrderingFilter, ) serializer_class = RollbackHistorySerializer ordering_fields = ("id", ) ordering = ('-id',) get_description = "回滚历史记录页" class RollbackHistoryDetailAPIView(RetrieveUpdateAPIView): queryset = RollbackHistory.objects.all()\ .prefetch_related("rollbackdetail_set") serializer_class = RollbackHistoryDetailSerializer lookup_url_kwarg = 'pk' get_description = "回滚历史记录详情页" put_description = "回滚重试" def update(self, request, *args, **kwargs): instance = self.get_object() serializer = RollbackTryAgainSerializer(instance, data=request.data) serializer.is_valid(raise_exception=True) rollback_service.delay(instance.id) return Response() class RollbackChoiceListAPIView(GenericAPIView): queryset = UpgradeDetail.objects.filter( upgrade_state__in=[ UpgradeStateChoices.UPGRADE_SUCCESS, UpgradeStateChoices.UPGRADE_FAIL ] ).exclude(has_rollback=True).exclude(service__isnull=True) filter_backends = (SearchFilter, RollBackHistoryFilter) search_fields = ("target_app__app_name", ) get_description = "可回滚服务列表页" def get(self, requests): queryset = self.filter_queryset(self.get_queryset()) upgrades_data = RollbackListSerializer(queryset, many=True).data service_id_max_d, service_name_max_d = {}, {} for upgrade_data in upgrades_data: service_id = upgrade_data.get("service_id") detail_id = upgrade_data.get("id") app_name = upgrade_data.get("app_name") service_max_data = service_id_max_d.get(service_id, {}) if service_max_data.get("id", float("-inf")) > detail_id: continue service_id_max_d[service_id] = upgrade_data if app_name not in service_name_max_d: service_name_max_d[app_name] = {service_id: upgrade_data} else: service_name_max_d[app_name].update({service_id: upgrade_data}) response_data = [ {"app_name": app_name, "children": list(max_info.values())} for app_name, max_info in service_name_max_d.items() ] return Response(data={"results": response_data}) class DoRollbackAPIView(GenericAPIView): get_description = "回滚服务" def post(self, requests): if UpgradeHistory.objects.filter( upgrade_state__in=[ UpgradeStateChoices.UPGRADE_WAIT, UpgradeStateChoices.UPGRADE_ING, ] ).exists(): raise GeneralError("存在正在升级的服务,请稍后!") if RollbackHistory.objects.filter( rollback_state__in=[ RollbackStateChoices.ROLLBACK_WAIT, RollbackStateChoices.ROLLBACK_ING, ] ).exists(): raise GeneralError("存在正在回滚的服务,请稍后!") choices = requests.data.get("choices", []) if not choices: raise GeneralError("请选择需要回滚的记录!") upgrade_details = UpgradeDetail.objects.filter( id__in=choices, upgrade_state__in=[ UpgradeStateChoices.UPGRADE_SUCCESS, UpgradeStateChoices.UPGRADE_FAIL ] ).values("id", "current_app_id", "union_server") if upgrade_details.count() != len(choices): raise GeneralError("提交信息校验失败,请刷新重试!") # 校验同一个服务是否回滚至同一版本 union_app = {} for detail in upgrade_details: rollback_app_id = detail.get("current_app_id") union_server = detail.get("union_server") if not union_server: raise GeneralError(f"实例{union_server}不在平台纳管范围!") if not union_app.get(union_server): union_app[union_server] = rollback_app_id continue if union_app.get(union_server) != rollback_app_id: raise GeneralError(f"实例{union_server}将回滚的服务版本不一致!") with transaction.atomic(): history = RollbackHistory.objects.create( env=Env.objects.first(), operator=requests.user ) RollbackDetail.objects.bulk_create( [ RollbackDetail( history=history, upgrade_id=upgrade_detail.get("id") ) for upgrade_detail in upgrade_details ] ) rollback_service.delay(history.id) return Response({"history": history.id}) ================================================ FILE: omp_server/services/__init__.py ================================================ ================================================ FILE: omp_server/services/admin.py ================================================ # from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/services/app_check/__init__.py ================================================ from .conf_check import ConfCheck from .manage_ser_exec import ManagerService __all__ = [ ConfCheck, ManagerService ] ================================================ FILE: omp_server/services/app_check/conf_check.py ================================================ import time import logging import json import random from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from utils.parse_config import THREAD_POOL_MAX_WORKERS from app_store.new_install_utils import RedisDB from utils.plugin.salt_client import SaltClient from utils.parse_config import SERVICE_DISCOVERY from db_models.models import ( ApplicationHub, Service ) logger = logging.getLogger('server') # 存在ip的筛选 class ConfCheck: def __init__(self, app_data): self.app_data = app_data self.redis_obj = RedisDB() self.need_key = {'base_dir', 'run_user', 'service_port', 'data_dir', 'log_dir'} self.app_dc = {} self.ips = {} self.response_ls = [] self.redis_save = [] # self.cluster_dc = {} self.app_ser = {} self.is_common_dc = {} self.is_base_env = ApplicationHub.objects.filter( is_base_env=True ).values_list("app_name", flat=True) self.is_common = ApplicationHub.objects.filter( is_base_env=False, app_type=ApplicationHub.APP_TYPE_COMPONENT).values_list("app_name", flat=True) self.service_ip_name = self._get_service_k_v() # self.service_cluster_ip = self._get_cluster_ip() self.error_ser = set() @staticmethod def _get_service_k_v(): service_dc = {} service_in_ip = {} service_ls = Service.objects.all().values_list("ip", "service__app_name", "service_instance_name") for k in service_ls: service_dc.setdefault(k[1], []).append(k[0]) service_in_ip.setdefault(k[1], {}).update({k[0]: k[2]}) return service_dc, service_in_ip @staticmethod def _get_cluster_ip(): """ "jdk:{1:["192.168.0.1","192.168.0.2"]}" """ app_dc = {} service_all = Service.objects.all().values_list("ip", "cluster", "cluster__cluster_service_name") for app in service_all: if app[1]: app_dc.setdefault(app[2], {}).setdefault(app[1], []).append(app[0]) return app_dc @staticmethod def explain_json(text): if text: return json.loads(text) return [] def fix_service(self): """ 整理格式 { app_name1:{ version:"xxx", base_dir:"xx", run_user:"xx", service_port:"xx" }, app_name2:{ ..... } } """ self.ips = self.app_data.pop("ips", []) for info in self.app_data.get("service", []): if info.get("child"): pro_ser_ls = list(info.get("child").values())[0] for app in pro_ser_ls: app_install_dc = {} name = app.pop("name") app_install_dc["version"] = app.pop("version") app_install_dc.update(app) self.app_dc[name] = app_install_dc else: version = {"version": info.pop("version")[0]} name = info.pop("name") info.update(version) self.app_dc[name] = info def fix_dependence(self): """ 整理依赖 app_name:{ version:"xxx", base_dir:"xx", run_user:"xx", service_port:"xx", de_app_id:[ "1","2" ] } """ app_ls = ApplicationHub.objects.filter(app_name__in=list(self.app_dc)). \ values_list("app_name", "app_dependence", "app_version") app_all_dc = {} # 前缀匹配多版本时使用最新版本,版本需要有已安装的服务. # "id", "app_name", "app_version", "service" app_all = ApplicationHub.objects.all().values_list( "id", "app_name", "app_version", "service__service_instance_name" ) for app in app_all: if app[3]: app_all_dc[f"{app[1]}:{app[2].split('.')[0]}"] = app[0] self.app_ser.setdefault(f"{app[1]}:{app[2]}", []).append(app[3]) # 前期校验完依赖存在,因此认定当前依赖的一定会在服务列表中找寻到 for new in app_ls: if self.app_dc[new[0]].get("version") != new[2]: continue for dependence in self.explain_json(new[1]): dependence_key = f'{dependence.get("name")}:{dependence.get("version")}' if app_all_dc.get(dependence_key): self.app_dc[new[0]].setdefault("de_app_id", []).append(app_all_dc.get(dependence_key)) def produce_cmd_and_exec(self, ip, agent_dir, install_args, app): """ 校验基本信息 1.目录 必须 2.用户 必须 3.端口存在 可选 """ # 过滤已经纳管的 if ip in self.service_ip_name[0].get(app, []): return True, "" salt_client = SaltClient() base_dir, is_success, message = \ install_args.get("base_dir").replace('{data_path}', agent_dir), False, "" if base_dir: cmd = [] dir_name = ["base_dir", "data_dir", "log_dir"] for _ in dir_name: dir_path = install_args.get(_).replace('{data_path}', agent_dir) install_args[_] = dir_path if dir_path: cmd.append(f"test -d {dir_path}||echo {dir_path}") cmd = "&&".join(cmd) is_success, message = salt_client.cmd(target=ip, command=cmd, timeout=10) if message: return False, f"{ip}:{app}:{message}目录不存在" if is_success and install_args.get("run_user"): f_dir, c_dir = base_dir.rsplit("/", 1) cmd = f"ls -l {f_dir} | grep {c_dir} | head -1 | awk '{{print $3}}'" is_success, message = salt_client.cmd(target=ip, command=cmd, timeout=10) if not is_success and message != install_args.get("run_user"): return False, f"{ip}:{app}:{install_args.get('run_user')}与当前用户{message}不匹配" if is_success and install_args.get("service_port"): cmd = f" 1 and app_name not in self.is_base_env: # is_mach = False # for ip_c in self.service_cluster_ip.get(app_name, {}).values(): # if set(ip_c) & set(ip) == set(ip): # is_mach = True # if not is_mach: # return False, f"当前依赖{app_name}节点处{ip}在数据库中不存在" # return True, "" # def dumps_scripts_args(self, instance_ls, ser_info, install_args, version, app): # """ # 校验成功数据录入 # """ # random_str = random.sample('ABCDEFGHIJKLMNQPQRSTUVWXYZ1234567890', 10) # first_ip = list(instance_ls)[0] # instance_name = f"{app}_{first_ip.split('.')[2]}_{first_ip.split('.')[3]}" if \ # len(instance_ls) == 1 else f"{list(ser_info['instance'])[0]}-cluster-{''.join(random_str)}" # dependence_name = [] # for a_p, ip in ser_info["dependence"].items(): # app_dc = {a_p: []} # app_all_dc = self.service_ip_name[1].get(a_p, {}) # for ip_d in ip: # app_dc[a_p].append(app_all_dc.get(ip_d, "")) # dependence_name.append(app_dc) # # 类似扩容逻辑 # app_ips = set(self.service_ip_name[0].get(list(ser_info['instance'])[0], [])) # finally_ip = list(instance_ls - app_ips) # self.response_ls.append({"name": app, # "ip": finally_ip, # "error": ""}) # self.redis_save.append({instance_name: list(instance_ls), # "dependence_instance": dependence_name, # "app_name": list(ser_info['instance'])[0], # "app_version": version, # "app_ip": finally_ip, # "error_msg": "", # "install_args": install_args}) # @staticmethod # def check_dump_de(new_de, old_de): # diff = set(new_de.keys()) & set(old_de.keys()) # if diff != set(new_de.keys()): # return f"服务同集群下不同节点采集到的依赖存在不同{','.join(diff)}" # for app_name, ip_list in new_de.items(): # # ToDo 临时修改 # if app_name == "zookeeper": # continue # if set(old_de[app_name]) & set(ip_list) != set(ip_list): # return f"相同集群下下不同节点采集到的依赖服务{app_name}地址存在差异" # def check_cluster(self, instance_ls, instance_list_set, dependence): # error_message = "" # for _ in instance_list_set: # if instance_ls == _[0]: # error_message = self.check_dump_de(dependence, _[1]) # return True, error_message # return False, error_message # def explain_scripts_res(self, message, app, install_args, ips, version, ip_list): # """ # zookeeper:[{"ip1","ip2"},{"ip3","ip4"}] # message: # {"instance": {"zookeeper": ["10.0.9.33"]}, "dependence": {"jdk": ["10.0.9.33"]}} # """ # ser_info = json.loads(message) # 代理节点,不存在配置文件中,但发现了安装路径的ip # instance_ls = set(ser_info["instance"].get(app, [])) | set(ips) # 期望纳管节点不在agent节点中 # app_db_all_ip = set(self.service_ip_name[0].get(app, [])) # if len((set(ip_list) | app_db_all_ip) & instance_ls) != len(instance_ls): # self.append_error(app, f"当前{app}存在问题:选择纳管节点{str(ip_list)}" # f"与期望纳管节点{instance_ls}不一致", instance_ls) # return # instance_list_set = self.cluster_dc.get(app, []) # error_message = None # if not instance_list_set: # instance_list_set = self.cluster_dc[app] = [[set(instance_ls), ser_info["dependence"]]] # res, message = self.explain_scripts_de(ser_info) # if not res: # error_message = f"当前{app}存在问题:{message}" # else: # self.dumps_scripts_args(instance_ls, ser_info, install_args, version, app) # # 首先我们查的是其中一个。那么我们应该在收集所有信息之后再进行增减。其余应该只做校验 # for index, instance in enumerate(instance_list_set): # # 过滤重复 查看重复依赖有无问题 # is_repeat, message = self.check_cluster(instance_ls, instance_list_set, ser_info["dependence"]) # if is_repeat: # if message: # error_message = message # continue # 非重复且无交叉 # intersection = instance_ls & instance[0] # if len(intersection) == 0: # instance_list_set.append([set(instance_ls), ser_info["dependence"]]) # res, message = self.explain_scripts_de(ser_info) # if not res: # error_message = message # else: # error_message = f"{app}集群存在交叉现象{intersection}" # # 已存在正确的要追加错误信息。 # if not error_message: # self.dumps_scripts_args(instance_ls, ser_info, install_args, version, app) # if error_message: # self.append_error(app, f"当前{app}存在问题:{error_message}", instance_ls) def explain_common_res(self, ips_ls, app, version, install_args): """ 通用自研和lib纳管 """ for ip in ips_ls: # instance_name = f"{app}_{ip.split('.')[2]}_{ip.split('.')[3]}" # instance_name: ip, res, de_dc = self.get_dependence(app, ip) error_msg = "" if res else f"{ip}缺少基础组件如jdk,comlib等" redis_dc = { "dependence_instance": de_dc, "app_name": app, "app_version": version, "app_ip": [ip], "error_msg": error_msg, "install_args": install_args } if app in self.is_common: if self.is_common_dc.get(app): self.is_common_dc[app]["app_ip"].append(ip) else: self.is_common_dc[app] = redis_dc continue self.response_ls.append({"name": app, "ip": [ip], "error": error_msg, "exist_instance": [], "is_use_exist": False}) self.redis_save.append(redis_dc) def append_error(self, app, message, ip_ls=None): if ip_ls: for k in self.response_ls: if list(ip_ls)[0] in k["ip"]: k["error"] = message return self.response_ls.append({"name": app, "ip": [], "error": message, "exist_instance": [], "is_use_exist": False }) def check_component_cmd(self, install_args, app): """ 进一步检查,并获取依赖信息 """ if not install_args.get("ip") and app in self.error_ser: self.append_error(app, f"当前{app}组件未发现,或所发现的节点都已存在服务列表中" f",请检查安装路径是否存在或取消扫描此服务") # 或需纳管的服务已全部纳管 return True, f"当前{app}组件不支持纳管" ips_ls = install_args.pop("ip", []) version = install_args.pop("version") # if app in SERVICE_DISCOVERY: # salt_client = SaltClient() # for ip in ips_ls: # cmd = f"{self.ips[ip]}/omp_salt_agent/env/bin/python3.8 " \ # f"{self.ips[ip]}/omp_salt_agent/scripts/{app}.py " \ # f"--base_dir {install_args.get('base_dir')} --local_ip {ip}" # is_success, message = salt_client.cmd(target=ip, command=cmd, timeout=10) # mysql需要每个节点都执行完后再验证。 # if is_success: # self.explain_scripts_res(message, app, install_args, [ip], version, ips_ls) # else: # self.append_error(app, f"当前{app}组件执行脚本获取参数失败{message}") # return False, message # 此处添加结果 # return True, "" # elif app in self.is_common: # self.append_error(app, f"当前{app}组件不支持纳管") # return False, f"当前{app}组件不支持纳管" # else: # 查找自研组件依赖,此时的ip一定是我们需要的ip self.explain_common_res(ips_ls, app, version, install_args) # 存一下redis 生成key return True, "" def run(self): """ 入口函数 """ self.fix_service() self.fix_dependence() # 初次校验 with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: _check_list_env = [] for ip, agent_dir in self.ips.items(): for app, install_args in self.app_dc.items(): future_obj = executor.submit( self.produce_cmd_and_exec, ip, agent_dir, install_args, app) _check_list_env.append(future_obj) for future in as_completed(_check_list_env): is_success, message = future.result() if not is_success: self.error_ser.add(message.split(":")[1]) logger.info(message) # 脚本和格式化校验 with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: _check_component_env = [] for app, install_args in self.app_dc.items(): future_obj = executor.submit( self.check_component_cmd, install_args, app) _check_component_env.append(future_obj) for future in as_completed(_check_component_env): is_success, message = future.result() if not is_success: logger.info(message) for app, info in self.is_common_dc.items(): exist_instance = self.app_ser.get(f"{app}:{info.get('app_version', '')}", []) self.response_ls.append({"name": app, "ip": info.get("app_ip", []), "error": info.get("error_msg", ""), "exist_instance": exist_instance, "is_use_exist": False if len(exist_instance) == 0 else True}) self.redis_save.append(info) self.redis_obj.set( name=str(int(time.time())), data=self.redis_save ) is_continue = True for ser in self.response_ls: if ser.get("error", None): is_continue = False return {"ser_info": self.response_ls, "uuid": str(int(time.time())), "is_continue": is_continue} ================================================ FILE: omp_server/services/app_check/manage_ser_exec.py ================================================ import json import os import copy import logging import time from django.db import transaction from db_models.models import ( MainInstallHistory, DetailInstallHistory, Service, ServiceConnectInfo, ApplicationHub, ClusterInfo, Env, ServiceHistory, Host ) import random from app_store.new_install_utils import RedisDB from app_store.tasks import add_prometheus logger = logging.getLogger('server') class ManagerService: def __init__(self, info, is_extend=False): self.info = info self.is_extend = is_extend self.redis_obj = RedisDB() self.redis_key = "extend_" + str(int(time.time())) if is_extend else info.get("uuid", "") self.data = self.check_redis()[1] self.service_cluster_ip, self.app_instance_dc = self._get_cluster_ip() self.env = None self.is_base_env = ApplicationHub.objects.filter( is_base_env=True).values_list("app_name", flat=True) @staticmethod def _get_cluster_ip(): """ "jdk:{1:["192.168.0.1","192.168.0.2"]}" """ app_dc = {} app_instance_dc = {} service_all = Service.objects.all().values_list("ip", "cluster", "cluster__cluster_service_name", "service_instance_name") for app in service_all: # 新纳管专属逻辑通过服务实例名匹配 app_instance_dc[app[3]] = app[1] if app[1]: app_dc.setdefault(app[2], {}).setdefault(app[1], []).append(app[0]) return app_dc, app_instance_dc def check_redis(self): if self.is_extend: return True, self.info res, redis_data = self.redis_obj.get(self.redis_key) if not res: return False, {"is_error": True, "message": "redis中不存在需要纳管的数据或超时"} # 兼容新版本纳管 for rd in redis_data: app_name = rd["app_name"] for i in self.info["ser_info"]: if i["name"] == app_name: rd.update({ "exist_instance": i["exist_instance"], "is_use_exist": i["is_use_exist"] }) return True, redis_data def check_service(self): for i in self.info["ser_info"]: if len(i["exist_instance"]) > 1: return False, { "is_error": True, "message": f"{i['name']}仅可勾选单个实例"} if i["is_use_exist"] and len(i["exist_instance"]) == 0: return False, { "is_error": True, "message": f"{i['name']}选择与现有实例组成集群,但未选择集群实例"} for data in self.data: if data.get("error_msg"): return False, { "is_error": True, "message": "存在有扫描异常的服务"} return True, "" @staticmethod def update_port(app_obj, app_data): service_ports = json.loads(app_obj.app_port) if app_obj.app_port else [] for service_port in service_ports: if service_port.get('key') == 'service_port': service_port['default'] = app_data['install_args'].get('service_port') return json.dumps(service_ports) @staticmethod def update_service_controllers(app_obj, app_data): _app_controllers = json.loads(app_obj.app_controllers) if app_obj.app_controllers else [] real_home = app_data['install_args'].get('base_dir') _new_controller = dict() for key, value in _app_controllers.items(): if not value: continue _new_controller[key] = os.path.join(real_home, value) return _new_controller @staticmethod def update_install_args(app_obj, install_detail_args): install_detail_keys = ["base_dir", "data_dir", "log_dir", "run_user"] json_install_args = json.loads(app_obj.app_install_args) for install_args in json_install_args: if install_args.get('key') in install_detail_keys: install_args['default'] = install_detail_args.get(install_args.get('key'), "") return json_install_args def create_install_detail(self, main_obj, install_detail_args, ser_obj, copy_data, app_obj): data_dir = Host.objects.filter(ip=ser_obj.ip).first().data_folder cluster_name = "" if ser_obj.cluster: cluster_name = ser_obj.cluster.cluster_service_name install_detail = { "ip": ser_obj.ip, "name": app_obj.app_name, "ports": json.loads(copy_data['service_port']), "version": app_obj.app_version, "install_args": self.update_install_args(app_obj, install_detail_args), "instance_name": ser_obj.service_instance_name, "data_folder": data_dir, "cluster_name": cluster_name } now_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(time.time())) install_status = DetailInstallHistory.INSTALL_STATUS_INSTALLING msg = "开始扩容" if not self.is_extend: install_status = DetailInstallHistory.INSTALL_STATUS_SUCCESS ServiceHistory.objects.create( username="admin", description=f"执行 纳管 操作", result="success", created=now_time, service=ser_obj ) msg = "纳管成功" DetailInstallHistory.objects.create( service=ser_obj, main_install_history=main_obj, install_step_status=install_status, send_msg=f"{now_time} {app_obj.app_name} {msg}", install_detail_args=install_detail ) def get_service_dependence(self, dependence, ip): dependence_json = [] for de in dependence: for app, hosts_name in de.items(): ser_obj = Service.objects.filter(service_instance_name=hosts_name[0]).first() # 是集群 if ser_obj.cluster: dependence_json.append({"name": app, "cluster_name": ser_obj.cluster.cluster_name, "instance_name": None }) # 基础组件本地 elif app in self.is_base_env: dependence_json.append({"name": app, "cluster_name": None, "instance_name": f"{app}-{ip.split('.')[2]}-{ip.split('.')[3]}" }) else: dependence_json.append({"name": app, "cluster_name": None, "instance_name": hosts_name[0] }) return json.dumps(dependence_json) @staticmethod def get_or_create_service_connect_info(app_name, app_obj): # ToDo 多环境连接信息问题 infos = {"username", "password", "username_enc", "password_enc"} connect_infos = {} for app_info in json.loads(app_obj.app_install_args): key = app_info.get("key") if key in infos: connect_infos[f"service_{key}"] = app_info.get("default") conn_obj = None if connect_infos: # ToDO 判断同同样连接信息时,当前服务归属哪个集群 conn_obj = ServiceConnectInfo.objects.filter( service_name=app_name, **connect_infos).first() if not conn_obj: conn_obj = ServiceConnectInfo.objects.create( service_name=app_name, **connect_infos ) return conn_obj def create_cluster_new(self, app_obj, app_data, app_name, real_ip_ls): # real_ip_ls 1 单机不考虑 # real_ip_ls 1 exist_instance 1 单机转集群合并 需创建集群 # real_ip_ls >1 exist_instance 0 集群 # real_ip_ls >1 exist_instance >0 集群合并 is_use_exist = app_data.pop("is_use_exist") exist_instance = app_data.pop("exist_instance") if is_use_exist or len(real_ip_ls) > 1: # 判断当前需要融合的实例是否存在集群,无则创建,必返回集群 exist_instance = exist_instance[0] if exist_instance else 0 cluster_id = self.app_instance_dc.get(exist_instance) # 没有集群需要创建,或者确定集群并且不合并集群的但数量大于1。 if not cluster_id or not is_use_exist: cluster_obj = ClusterInfo.objects.create( cluster_service_name=app_name, cluster_name=f"{app_name}-cluster-{''.join(random.sample('ABCDEFGHIJKLMNQPQRSTUVWXYZ1234567890', 10))}", service_connect_info=self.get_or_create_service_connect_info(app_name, app_obj) ) # 实例存在但不存在集群 if exist_instance: Service.objects.filter( service_instance_name=exist_instance).update(cluster=cluster_obj) else: cluster_obj = ClusterInfo.objects.filter(id=cluster_id).first() return True, cluster_obj # 看大小返回 else: return False, [f"{app_name}-{real_ip_ls[0].split('.')[2]}-{real_ip_ls[0].split('.')[3]}"] def create_cluster(self, app_obj, app_data, app_name, real_ip_ls): cluster_instance_name = [] for key, v in app_data.items(): if key.startswith(f'{app_name}'): cluster_instance_name = [key, v] if cluster_instance_name: app_data.pop(cluster_instance_name[0]) if not app_data.get("is_use_exist") is None: return self.create_cluster_new(app_obj, app_data, app_name, real_ip_ls) # 此为纳管集群 cluster_obj = None if set(cluster_instance_name[1]) & set(real_ip_ls) != set(cluster_instance_name[1]): for c_id, ips in self.service_cluster_ip.get(app_name, {}).items(): if list(set(cluster_instance_name[1]) - set(real_ip_ls))[0] in ips: cluster_obj = ClusterInfo.objects.filter(id=c_id).first() elif len(cluster_instance_name[1]) > 1: cluster_obj = ClusterInfo.objects.create( cluster_service_name=app_name, cluster_name=cluster_instance_name[0], service_connect_info=self.get_or_create_service_connect_info(app_name, app_obj) ) else: return False, cluster_instance_name return True, cluster_obj def get_or_create_env(self): if self.env: return self.env queryset = Env.objects.filter(id=1) if queryset.exists(): return queryset.first() return Env.objects.create(id=1, name="default") def create_database_one(self, data, main_obj): app_name = data.pop("app_name") app_version = data.pop("app_version") real_ip_ls = data.pop("app_ip", []) dependence = data.pop("dependence_instance") app_obj = ApplicationHub.objects.filter( app_name=app_name, app_version=app_version).first() data['service_port'] = self.update_port(app_obj, data) data['service_controllers'] = self.update_service_controllers(app_obj, data) data['env'] = self.get_or_create_env() res, cls_obj = self.create_cluster(app_obj, data, app_name, real_ip_ls) if res: data["cluster"] = cls_obj else: data["service_instance_name"] = cls_obj[0] data["service_connect_info"] = self.get_or_create_service_connect_info(app_name, app_obj) data["service_status"] = Service.SERVICE_STATUS_INSTALLING \ if self.is_extend else Service.SERVICE_STATUS_NORMAL for ip in real_ip_ls: # ToDo 未设置vip的选项 copy_data = copy.deepcopy(data) if not copy_data.get("service_instance_name"): copy_data["service_instance_name"] = f"{app_name}-{ip.split('.')[2]}-{ip.split('.')[3]}" copy_data["service_dependence"] = self.get_service_dependence(dependence, ip) copy_data['service'] = app_obj copy_data["ip"] = ip # ToDo Role 和 vip的 copy_data["service_role"] = "" install_detail_args = copy_data.pop("install_args") ser_obj = Service.objects.create( **copy_data ) # 创建detail表 self.create_install_detail(main_obj, install_detail_args, ser_obj, copy_data, app_obj) def create_database_all(self): # main表 相关联的服务表 信息集群表,然后detail表 with transaction.atomic(): main_obj = MainInstallHistory.objects.create( operator="admin", operation_uuid=self.redis_key, install_status=MainInstallHistory.INSTALL_STATUS_INSTALLING ) # "dependence_instance": de_dc, # "app_name": app, # "app_version": version, # "app_ip": [ip], # "install_args": install_args for data in self.data: data.pop("error_msg") self.create_database_one(data, main_obj) if not self.is_extend: main_obj.install_status = MainInstallHistory.INSTALL_STATUS_SUCCESS main_obj.save() try: add_prometheus(main_obj.id) except Exception as e: logger.error(f"纳管服务注册失败:{e}") return main_obj.id def run(self): res, data = self.check_redis() if res: res, data = self.check_service() if not res: return data main_id = self.create_database_all() if self.is_extend: return main_id self.redis_obj.delete_keys(self.redis_key) return {"is_error": False, "message": "服务纳管成功"} ================================================ FILE: omp_server/services/apps.py ================================================ from django.apps import AppConfig class ServicesConfig(AppConfig): name = 'services' ================================================ FILE: omp_server/services/permission.py ================================================ from django.conf import settings from rest_framework.permissions import BasePermission class GetDataJsonAuthenticated(BasePermission): """ Allows access only for users who with secret or authentication . """ def has_permission(self, request, view): query_field = request.query_params.get("secret", "") return query_field == settings.DATA_JSON_SECRET or \ bool(request.user and request.user.is_authenticated) ================================================ FILE: omp_server/services/self_heal_filter.py ================================================ import time import django_filters from db_models.models import SelfHealingHistory from django_filters.rest_framework import FilterSet from rest_framework.filters import BaseFilterBackend class SelfHealingHistoryFilter(FilterSet): """自愈历史记录过滤类""" host_ip = django_filters.CharFilter( help_text="HOST_IP,模糊匹配", field_name="host_ip", lookup_expr="icontains" ) state = django_filters.CharFilter(help_text="STATE,模糊匹配", field_name="state", lookup_expr="icontains") instance_name = django_filters.CharFilter(help_text="INSTANCE_NAME,模糊匹配", field_name="instance_name", lookup_expr="icontains") class Meta: model = SelfHealingHistory fields = ("host_ip", "state", "instance_name") class SelfHealingTimeFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): query_start_time = request.GET.get("query_start_time", "") query_end_time = request.GET.get("query_end_time", "") if query_start_time and query_end_time: try: time.strptime(query_start_time, "%Y-%m-%d %H:%M:%S") time.strptime(query_end_time, "%Y-%m-%d %H:%M:%S") except ValueError: return queryset.all() return queryset.filter(alert_time__range=(query_start_time, query_end_time)) return queryset.all() ================================================ FILE: omp_server/services/self_heal_serializers.py ================================================ import logging from rest_framework import serializers from rest_framework.serializers import ModelSerializer, Serializer from db_models.models import SelfHealingSetting, SelfHealingHistory from rest_framework_bulk import BulkSerializerMixin, BulkListSerializer from rest_framework.exceptions import ValidationError logger = logging.getLogger("server") class SelfHealingSettingSerializer(BulkSerializerMixin, ModelSerializer): class Meta: model = SelfHealingSetting fields = "__all__" list_serializer_class = BulkListSerializer def validate(self, attrs): repair_ls = SelfHealingSetting.objects.all().values_list("repair_instance", flat=True) repairs = [] for _ in repair_ls: repairs.extend(_) repeat = set(repairs) & set(attrs["repair_instance"]) if repeat: raise ValidationError(f"服务不可重复{repeat}") if "all" in repairs: raise ValidationError(f"all服务不可再次添加其他服务") return attrs class ListSelfHealingHistorySerializer(ModelSerializer): class Meta: model = SelfHealingHistory fields = "__all__" class UpdateSelfHealingHistorySerializer(Serializer): """自愈历史记录批量更新""" ids = serializers.ListField(help_text="自愈历史记录ID列表", required=True, error_messages={"required": "必须包含ID列表字段"}) is_read = serializers.IntegerField(help_text="是否已读", required=True, error_messages={"required": "必须包含是否已读字段"}) def create(self, validated_data): SelfHealingHistory.objects.filter(id__in=validated_data.get("ids")).update( is_read=validated_data.get("is_read")) return validated_data ================================================ FILE: omp_server/services/self_heal_util.py ================================================ import logging import requests import json from utils.parse_config import MONITOR_PORT from promemonitor.prometheus_utils import CW_TOKEN logger = logging.getLogger("server") def get_service_status_direct(service_obj_list): """ 直接从monitor_agent获取服务状态 param: [{"ip": "127.0.0.1", "service_name": "mysql"}, {"ip": "127.0.0.1", "service_name": "redis"}] """ service_obj_result = list() monitor_agent_port = MONITOR_PORT.get('monitorAgent', 19031) headers = {"Content-Type": "application/json"}.update(CW_TOKEN) ip_item_list = list() ip_list = list() for ele in service_obj_list: ip_list.append(ele.get("ip")) ip_list = list(set(ip_list)) for ip in ip_list: ip_service_list = list() for item in service_obj_list: if ip == item.get("ip"): ip_service_list.append(item) ip_item_list.append(ip_service_list) try: for ii in ip_item_list: status_url = f"http://{ii[0].get('ip')}:{monitor_agent_port}/service_status" # NOQA response = requests.request( "POST", status_url, headers=headers, data=json.dumps(ii)) if response.status_code != 200: continue service_obj_result.extend(response.json().get("beans")) return service_obj_result except Exception as e: logger.error(f"获取制定服务列表状态失败,详情为:{e}") return service_obj_list ================================================ FILE: omp_server/services/self_heal_view.py ================================================ import logging import json from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import OrderingFilter from rest_framework.viewsets import GenericViewSet from rest_framework.response import Response from rest_framework.mixins import CreateModelMixin, ListModelMixin, DestroyModelMixin, UpdateModelMixin from services.self_heal_serializers import SelfHealingSettingSerializer, ListSelfHealingHistorySerializer, \ UpdateSelfHealingHistorySerializer from services.self_heal_filter import SelfHealingTimeFilter, SelfHealingHistoryFilter from db_models.models import SelfHealingSetting, SelfHealingHistory from utils.common.paginations import PageNumberPager from services.self_healing import get_enable_health from rest_framework_bulk import BulkUpdateAPIView logger = logging.getLogger("server") class SelfHealingSettingView(GenericViewSet, ListModelMixin, CreateModelMixin, BulkUpdateAPIView, DestroyModelMixin, UpdateModelMixin): """自愈策略""" queryset = SelfHealingSetting.objects.all() serializer_class = SelfHealingSettingSerializer # 操作信息描述 get_description = "自愈策略" post_description = "自愈策略" def list(self, request, *args, **kwargs): query_field = request.query_params.get("instance", False) if query_field: # 查看还有多少服务需要 repair_ls = self.get_queryset().values_list("repair_instance", flat=True) repairs = [] for _ in repair_ls: repairs.extend(_) if "all" in repairs: data = {"all": False, "service_name": []} else: data = {"all": False if repairs else True, "service_name": get_enable_health(repairs) } return Response(data) else: return super(SelfHealingSettingView, self ).list(request, *args, **kwargs) class ListSelfHealingHistoryView(GenericViewSet, ListModelMixin): """自愈历史记录""" queryset = SelfHealingHistory.objects.all().order_by("-alert_time", "-end_time") serializer_class = ListSelfHealingHistorySerializer pagination_class = PageNumberPager filter_backends = ( DjangoFilterBackend, OrderingFilter, SelfHealingTimeFilter, ) filter_class = SelfHealingHistoryFilter ordering_fields = ("host_ip", "instance_name", "state", "alert_time", "end_time") class UpdateSelfHealingHistoryView(CreateModelMixin, GenericViewSet): """更新自愈历史记录试图""" serializer_class = UpdateSelfHealingHistorySerializer queryset = SelfHealingHistory.objects.all().order_by("id") post_description = "更新自愈历史记录(已读/未读)" ================================================ FILE: omp_server/services/self_healing.py ================================================ import logging import copy import time import paramiko import requests import json import datetime from db_models.models import Service, SelfHealingSetting, \ SelfHealingHistory, Host, WaitSelfHealing, ApplicationHub from celery import shared_task from promemonitor.prometheus_utils import CW_TOKEN from utils.plugin.salt_client import SaltClient from utils.plugin.crypto import AESCryptor from concurrent.futures import ( ThreadPoolExecutor, as_completed ) from utils.parse_config import MONITOR_PORT, BASIC_ORDER, \ THREAD_POOL_MAX_WORKERS, HEALTH_REDIS_TIMEOUT, HEALTH_REQUEST_COUNT, HEALTH_REQUEST_SLEEP from app_store.new_install_utils import RedisDB from utils.plugin.crontab_utils import maintain logger = logging.getLogger('server') class SelfHealing: def __init__(self, instance_tp, max_healing_count): # 需要起停信息ip等操作 self.instance_tp = instance_tp self.max_healing_count = max_healing_count self.redis = RedisDB() self.service_info = set() self.host_info = set() def merge_and_filter_ser(self, alert_info): """ 初始化用,过滤服务(主机),及服务初始化信息 """ data_dict = {} host_ls = [] for d in alert_info: if d.get("alert_service_name") and d.get("alert_instance_name") and \ d['alert_instance_name'] not in self.service_info: data_dict.setdefault(d['alert_service_name'], []).append(d) self.service_info.add(d['alert_instance_name']) if not d.get("alert_service_name") and \ d['alert_host_ip'] not in self.host_info: self.host_info.add(d['alert_host_ip']) host_ls.append(d) # 初始化主机和服务信息 self.host_info = dict(Host.objects.filter( ip__in=list(self.host_info)).values_list("ip", "agent_dir")) self.service_info = dict(Service.objects.filter( service_instance_name__in=list(self.service_info) ).values_list("service_instance_name", "service_controllers")) return data_dict, host_ls def sort_service(self, alert_info): """ 初始化用,提供排序 """ sort_dict = copy.copy(BASIC_ORDER) # 赋值并过滤 存在问题 过滤需要过滤会过滤到同服务不同实例名的例 data_dict, host_ls = self.merge_and_filter_ser(alert_info) sort_ser = [] for key in sort_dict: temp = [] for item in sort_dict[key]: temp.extend(data_dict.pop(item, [])) if temp: sort_ser.append(temp) other_ser = data_dict.values() if other_ser: other_ser = [service for app in other_ser for service in app] sort_ser.append(other_ser) if host_ls: sort_ser.insert(0, host_ls) return sort_ser def exec_salt_cmd(self, ip, command, his_obj): """ 进行启动,请求接口查询状态,日志追加,状态变更 """ salt_obj = SaltClient() cmd_flag, cmd_msg = salt_obj.cmd( target=ip, command=command, timeout=60) healing_log = f"执行ip:{ip},执行cmd:{command},执行结果:{cmd_flag},执行详情:{cmd_msg}," if not cmd_flag: his_obj.healing_log = healing_log his_obj.save() return False # 循环检测,超出检测时常后退出 for _ in range(HEALTH_REQUEST_COUNT): res = self.check_health(ip, command, his_obj, healing_log) if res: return True time.sleep(HEALTH_REQUEST_SLEEP) return False @staticmethod def check_health(ip, command, his_obj, healing_log): request_monitor = {"service_name": his_obj.service_name, "ip": ip} if "omp_monitor_agent" in command: request_monitor["service_name"] = "node" try: monitor_agent_res = get_service_status_direct([request_monitor]) except Exception as e: his_obj.healing_log = healing_log + ",请求监控报错" his_obj.save() logger.info("monitor_agent_res_error 监控报错信息{}".format(e)) return False if monitor_agent_res[0].get("status") == 1: his_obj.healing_log = healing_log + "monitor_agent_res 服务状态查看正常更新服务状态" his_obj.save() return True return False def get_command(self, instance_name): """ 获取cmd ,通过策略类型选择启动或重启 """ action_dc = { 0: "start", 1: "restart" } action = action_dc[self.instance_tp] service_action = self.service_info.get(instance_name) host_action = self.host_info.get(instance_name) if service_action: command = service_action.get(action) if service_action.get(action) \ else service_action.get("start", "").replace("start", action) else: command = f"bash {host_action}/omp_monitor_agent/monitor_agent.sh {action}" return command @staticmethod def write_db(data, healing_count): """ 合法后创建记录表用 """ initial_v = { "start_time": datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "is_read": 0, "state": 2, "healing_log": "", "healing_count": healing_count } compare_dc = { "alert_host_ip": "host_ip", "alert_service_name": "service_name", "alert_time": "alert_time", # "fingerprint": "fingerprint", # "monitor_log": "monitor_log", "alert_describe": "alert_content", "alert_instance_name": "instance_name", } write_db_dc = {} for field, value in data.items(): compare_v = compare_dc.get(field) if compare_v: write_db_dc[compare_v] = value write_db_dc.update(initial_v) return SelfHealingHistory.objects.create(**write_db_dc) def get_redis_count(self, instance_name): """ 缓存校验用,周期内自愈次数最大限制 """ _flag, _data = self.redis.get(f"heal{instance_name}") if not _flag: count = 1 else: count = int(_data) + 1 self.redis.update( name=f"heal{instance_name}", data=count, timeout=HEALTH_REDIS_TIMEOUT ) return count def host(self, hosts_info): identify_des = "monitor_agent进程丢失" ip = hosts_info["alert_host_ip"] if identify_des not in hosts_info.get('alert_describe'): # 暂不支持的模式 logger.info(f"暂不支持的模式{ip}") return True ip = hosts_info["alert_host_ip"] host_ip = "".join(ip.split(".")) healing_count = self.get_redis_count(host_ip) if healing_count > self.max_healing_count: # 自愈实例超出限制个数 return True # 写库 his_obj = self.write_db(hosts_info, healing_count) res = self.exec_salt_cmd(ip, self.get_command(ip), his_obj) if not res: return self.exec_salt_cmd(ip, self.get_command(ip), his_obj) his_obj.state = SelfHealingHistory.HEALING_SUCCESS if res else SelfHealingHistory.HEALING_FAIL his_obj.end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) his_obj.save() def service(self, service_info): # 获取service的redis的key HEALTH_REDIS_TIMEOUT identify_des = "been down for more than a minute" if identify_des not in service_info.get('alert_describe'): logger.info(f"暂不支持的模式{service_info['alert_instance_name']}") # 暂不支持的模式 return True healing_count = self.get_redis_count(service_info['alert_instance_name']) if healing_count > self.max_healing_count: # 自愈实例超出限制个数 logger.info(f"自愈实例超出限制个数{service_info['alert_instance_name']}") return True # 写库 his_obj = self.write_db(service_info, healing_count) command = self.get_command(service_info['alert_instance_name']) res = self.exec_salt_cmd(service_info['alert_host_ip'], command, his_obj) if not res: # 二次重复不再进行其余操作 res = self.exec_salt_cmd(service_info['alert_host_ip'], command, his_obj) his_obj.state = SelfHealingHistory.HEALING_SUCCESS if res else SelfHealingHistory.HEALING_FAIL his_obj.end_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) his_obj.save() def get_enable_health(repairs): h_ls = set(Host.objects.all().values_list("ip", flat=True)) for app in ApplicationHub.objects.filter(is_base_env=False): if not app.extend_fields.get("affinity", "") == "tengine": h_ls.add(app.app_name) return list(h_ls - set(repairs)) @shared_task @maintain def self_healing(task_id): # 校验是否需要自愈 self_obj = SelfHealingSetting.objects.get(id=task_id) if not self_obj.used: return "该策略并未启用" if "all" in self_obj.repair_instance: data = self_obj.get_enable_health(list()) else: data = list(self_obj.repair_instance) wait_ser = WaitSelfHealing.objects.filter(service_name__in=data) if not wait_ser or wait_ser.filter(repair_status=1): return "存在正在自愈的服务或无需自愈的服务" wait_ser.update(repair_status=1) repair_ser_dc = dict(wait_ser.values_list("id", "repair_ser")) repair_info = [] for ser in repair_ser_dc.values(): repair_info.append(ser) logger.info(f"需要自愈的服务:{repair_info}") # 排序 先主机 - 基础组件 -自研服务 try: health_obj = SelfHealing(self_obj.instance_tp, self_obj.max_healing_count) ser = health_obj.sort_service(repair_info) logger.info(f"等待自愈的服务信息:{ser}") # 开启修复 for service_ls in ser: with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: future_list = [] for service in service_ls: future_obj = executor.submit( getattr(health_obj, "service" if service['alert_type'] == "component" else service['alert_type']), service) future_list.append(future_obj) for future in as_completed(future_list): future.result() except Exception as e: logger.info(f"未知异常,需保护释放锁 {e}") # 释放锁 ,防止惰性查询再次筛选 WaitSelfHealing.objects.filter(id__in=list(repair_ser_dc)).delete() def self_healing_ssh_verification(host_self_healing_list, sudo_check_cmd): """ 先留着吧暂时没啥用。 """ host_self_healing_list = host_self_healing_list aes_crypto = AESCryptor() host_list = Host.objects.filter(ip=host_self_healing_list).values_list("ip", "port", "username", "password") for i in range(len(host_list)): try: if len(host_list[i][2]) != 0 and len(host_list[i][3]) != 0: client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(hostname=host_list[i][0], port=host_list[i][1], username=host_list[i][2], password=aes_crypto.decode(host_list[i][3]), timeout=60) """ 监控启动脚本 是否需要重启多次?""" sudo_check_cmd = sudo_check_cmd stdin, stdout, stderr = client.exec_command(sudo_check_cmd) stdout = stdout.read().decode('utf-8') stderr = stderr.read().decode('utf-8') """ 输出信息需要修改""" if "dead" in stdout: """ 监控未 启动 """ logger.info("monitor_agent 启动失败,输出信息:{} ".format(stdout)) return True, 0 if "running" in stdout: """ 监控启动""" return True, 1 else: logger.info("监控重启失败 无ssh") """ 无ssh的情况""" return False, 0 except Exception as e: logger.info("监控重启失败,报错信息为:{}".format(e)) """ ssh 连接超时""" return False, 1 return True, 2 def get_service_status_direct(service_obj_list): """ 直接从monitor_agent获取服务状态 param: [{"ip": "127.0.0.1", "service_name": "mysql"}, {"ip": "127.0.0.1", "service_name": "redis"}] """ service_obj_result = list() monitor_agent_port = MONITOR_PORT.get('monitorAgent', 19031) headers_type = {"Content-Type": "application/json"} headers_authentication = CW_TOKEN headers = dict(headers_type, **headers_authentication) ip_item_list = list() ip_list = list() for ele in service_obj_list: ip_list.append(ele.get("ip")) ip_list = list(set(ip_list)) for ip in ip_list: ip_service_list = list() for item in service_obj_list: if ip == item.get("ip"): ip_service_list.append(item) ip_item_list.append(ip_service_list) try: for ii in ip_item_list: status_url = f"http://{ii[0].get('ip')}:{monitor_agent_port}/service_status" # NOQA response = requests.request( "POST", status_url, headers=headers, data=json.dumps(ii)) logger.info("interface_monitor_agent监控接口返回数据:{}".format(response)) logger.info("interface_monitor_agent请求地址 {}".format(status_url)) if response.status_code != 200: continue logger.info("interface_monitor_agent 接口返回:{}".format(response.json())) service_obj_result.extend(response.json().get("beans")) return service_obj_result except Exception as e: logger.error(f"interface_monitor_agent 获取制定服务列表状态失败,详情为:{e}") return service_obj_list ================================================ FILE: omp_server/services/services_filters.py ================================================ """ 服务相关过滤器 """ import django_filters from django_filters.rest_framework import FilterSet from db_models.models import Service class ServiceFilter(FilterSet): """ 服务过滤类 """ ip = django_filters.CharFilter( help_text="IP,模糊匹配", field_name="ip", lookup_expr="icontains") service_instance_name = django_filters.CharFilter( help_text="服务实例名称,模糊匹配", field_name="service_instance_name", lookup_expr="icontains") label_name = django_filters.CharFilter( help_text="功能模块", field_name="service__app_labels__label_name", lookup_expr="icontains") app_type = django_filters.CharFilter( help_text="服务类型: 0-组件 1-应用", field_name="service__app_type", lookup_expr="exact") class Meta: model = Service fields = ("ip",) ================================================ FILE: omp_server/services/services_serializers.py ================================================ """ 服务序列化器 """ import json from rest_framework import serializers from db_models.models import Service, ApplicationHub from utils.common.serializers import DynamicFieldsModelSerializer class ServiceStatusSerializer(DynamicFieldsModelSerializer): is_web = serializers.SerializerMethodField() is_base_env = serializers.BooleanField(source="service.is_base_env") service_status = serializers.CharField(source="get_service_status_display") app_version = serializers.CharField(source="service.app_version") app_name = serializers.CharField(source="service.app_name") class Meta: """ 元数据 """ model = Service fields = ( "ip", "app_name", "app_version", "service_status", "is_base_env", "is_web", "service_instance_name" ) def get_is_web(self, obj): """ 或是是否为 web 服务 """ if obj.service.extend_fields.get("affinity", "") == "tengine": return True return False class ServiceSerializer(serializers.ModelSerializer): """ 服务序列化器 """ port = serializers.SerializerMethodField() label_name = serializers.SerializerMethodField() cluster_type = serializers.SerializerMethodField() alert_count = serializers.SerializerMethodField() operable = serializers.SerializerMethodField() is_web = serializers.SerializerMethodField() is_base_env = serializers.BooleanField(source="service.is_base_env") service_status = serializers.CharField(source="get_service_status_display") app_type = serializers.IntegerField(source="service.app_type") app_name = serializers.CharField(source="service.app_name") app_version = serializers.CharField(source="service.app_version") env = serializers.CharField(source="env.name") class Meta: """ 元数据 """ model = Service fields = ( "id", "service_instance_name", "ip", "port", "label_name", "alert_count", "operable", "app_type", "app_name", "app_version", "cluster_type", "service_status", "is_base_env", "is_web", "env" ) def get_is_web(self, obj): """ 或是是否为 web 服务 """ if obj.service.extend_fields.get("affinity", "") == "tengine": return True return False def get_port(self, obj): """ 返回服务 service_port """ service_port = "-" if obj.service_port is not None: service_port_ls = json.loads(obj.service_port) if len(service_port_ls) > 0: service_port = service_port_ls[0].get("default", "") return service_port def get_label_name(self, obj): """ 拼接返回标签 """ label_name = "-" if obj.service.app_labels.exists(): label_name = ", ".join( obj.service.app_labels.values_list("label_name", flat=True)) return label_name def get_cluster_type(self, obj): """ 获取集群类型 """ cluster_type = "单实例" if obj.cluster is not None: # cluster_type = obj.cluster.cluster_type cluster_type = "集群" return cluster_type def get_alert_count(self, obj): """ 获取告警数量 """ alert_count = f"{obj.alert_count}次" # 服务状态为 '安装中'、'安装失败' 告警数量显示为 '-' if obj.service_status in ( Service.SERVICE_STATUS_INSTALLING, Service.SERVICE_STATUS_INSTALL_FAILED): alert_count = "-" # '基础环境' 展示为 '-' base_env = obj.service.extend_fields.get("base_env", "") if isinstance(base_env, str): base_env = base_env.lower() if base_env in (True, "true"): alert_count = "-" return alert_count def get_operable(self, obj): """ 服务可操作 (启动、停止、重启) """ if obj.service_controllers is not None: return obj.service_controllers.get("start", "") != "" return False class ServiceDetailSerializer(serializers.ModelSerializer): """ 服务详情序列化器 """ app_name = serializers.CharField(source="service.app_name") app_version = serializers.CharField(source="service.app_version") label_name = serializers.SerializerMethodField() cluster_type = serializers.SerializerMethodField() install_info = serializers.SerializerMethodField() history = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = Service fields = ( "id", "service_instance_name", "app_name", "app_version", "label_name", "cluster_type", "ip", "install_info", "history", "created", ) def get_install_info(self, obj): """ 安装信息 """ result = { "service_port": "-", "base_dir": "-", "log_dir": "-", "data_dir": "-", "username": "-", "password": "-", } # 获取服务端口号 if obj.service_port is not None: service_port_ls = json.loads(obj.service_port) if len(service_port_ls) > 0: result["service_port"] = service_port_ls[0].get("default", "") # 应用安装参数 app_install_args = [] if obj.detailinstallhistory_set.exists(): detail_obj = obj.detailinstallhistory_set.first() app_install_args = detail_obj.install_detail_args.get( "install_args", []) for app_install_info in app_install_args: key = app_install_info.get("key", "") if key in result.keys(): result[key] = app_install_info.get("default", "-") return result def get_label_name(self, obj): """ 获取拼接后的标签 """ label_name = "-" if obj.service.app_labels.exists(): label_name = ", ".join( obj.service.app_labels.values_list("label_name", flat=True)) return label_name def get_cluster_type(self, obj): """ 获取集群类型 """ cluster_type = "-" if obj.cluster is not None: cluster_type = obj.cluster.cluster_type return cluster_type def get_history(self, obj): """ 获取历史记录 """ return list(obj.servicehistory_set.values( "username", "description", "result", "created")) class ServiceActionSerializer(serializers.ModelSerializer): """ 服务动作序列化类 """ class Meta: """ 元数据 """ model = Service fields = '__all__' class ServiceDeleteSerializer(serializers.ModelSerializer): """ 服务删除序列化类 """ class Meta: """ 元数据 """ model = Service fields = '__all__' class AppListSerializer(serializers.ModelSerializer): class Meta: """ 元数据 """ model = ApplicationHub fields = '__all__' ================================================ FILE: omp_server/services/tasks.py ================================================ """ 服务相关异步任务 """ import logging from celery import shared_task from celery.utils.log import get_task_logger from db_models.models import ( Service, ServiceHistory ) from utils.plugin.salt_client import SaltClient import time import json from promemonitor.prometheus_utils import PrometheusUtils from db_models.models import ( Host, HostOperateLog, ClusterInfo, Product, SelfHealingHistory, Alert ) from utils.parse_config import BASIC_ORDER, CLEAR_DB from django.utils import timezone from django.db.models import F from django.db import transaction # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") def delete_action(service_obj): """ 查询删除目录 """ install_detail = service_obj.detailinstallhistory_set.first().install_detail_args dir_list = ["base_dir", "log_dir", "data_dir"] valida_rm = [] for args in install_detail.get("install_args"): if args.get("key") in dir_list: dir_name = args.get("default") if dir_name and len(dir_name) >= 5: valida_rm.append(dir_name) result = " ".join(valida_rm) return result def get_app_dir(service_obj): """获取服务app_dir""" base_dir = "" install_detail_args = service_obj.detailinstallhistory_set.first().install_detail_args base_dir_dict = { "base_dir": args.get("default") for args in install_detail_args.get("install_args") if args.get("key") == "base_dir" } base_dir = base_dir_dict.get("base_dir", "") if base_dir and len(base_dir) >= 5: return base_dir def delete_file(service_controllers, service_obj): """ 删除文件操作 """ salt_obj = SaltClient() exe_action = service_controllers.get("stop", "") if "hadoop" in exe_action: scripts_param = exe_action.split() if len(scripts_param) > 3: return True scripts_param[2] = "all" exe_action = " ".join(scripts_param) # 存在stop脚本先执行stop脚本后执行删除 if exe_action: for count in range(2): is_success, info = salt_obj.cmd(service_obj.ip, exe_action, 600) time.sleep(count + 1) if is_success is True: break logger.info(f"执行 [delete] 操作 {is_success},原因: {info}") base_dir = delete_action(service_obj) app_dir = get_app_dir(service_obj) # TODO 删除定时任务 cron_del_str = f"crontab -l |grep -v {app_dir} 2>/dev/null | crontab -" cmd_res, msg = salt_obj.cmd( service_obj.ip, cron_del_str, 600 ) logger.info(f"执行 [delete] crontab操作 {cmd_res}, 原因: {msg}") # 删除安装路径 if base_dir: is_success, info = salt_obj.cmd( service_obj.ip, f"/bin/rm -rf {base_dir}", 600) logger.info(f"执行 [delete] 操作 {is_success},原因: {info}") return cmd_res and is_success @shared_task def exec_action(action, instance, operation_user, del_file=False, need_sleep=True): # edit by vum: 增加服务的目标成功状态、失败状态 action_json = { "1": ["start", 1, 0, 4], "2": ["stop", 2, 4, 0], "3": ["restart", 3, 0, 4], "4": ["delete", 4] } result_json = { True: "success", False: "failure" } try: service_obj = Service.objects.get(id=instance) except Exception as e: logger.error(f"service实例id,不存在{instance}:{e}") return None ip = service_obj.ip # service_controllers 字段为json字段类型 service_controllers = service_obj.service_controllers action = action_json.get(str(action)) if not action: logger.error("action动作不合法") raise ValueError("action动作不合法") if action[0] == 'delete': service_port = None if service_obj.service_port is not None: service_port_ls = json.loads(service_obj.service_port) if len(service_port_ls) > 0: service_port = service_port_ls[0].get("default", "") if service_port is not None: # 端口存在则删除prometheus监控的 ser_name = service_obj.service.app_name if ser_name == "hadoop": ser_name = service_obj.service_instance_name.split("_", 1)[0] service_data = { "service_name": ser_name, "instance_name": service_obj.service_instance_name, "data_path": None, "log_path": None, "env": service_obj.env.name, "ip": ip, "listen_port": service_port } PrometheusUtils().delete_service(service_data) # 删除hosts实例个数 service_history_obj = ServiceHistory.objects.filter( service=service_obj) if len(service_history_obj) != 0: service_history_obj.delete() if del_file: is_success = delete_file(service_controllers, service_obj) else: is_success = True host_instances = Host.objects.filter(ip=service_obj.ip) for instance in host_instances: HostOperateLog.objects.create(username=operation_user, description=f"卸载服务 [{service_obj.service.app_name}]", result="success" if is_success else "failed", host=instance) with transaction.atomic(): service_obj.delete() count = Service.objects.filter(ip=service_obj.ip).count() Host.objects.filter(ip=service_obj.ip).update( service_num=count) # 当服务被删除时,应该将其所在的集群都连带删除 if service_obj.cluster and Service.objects.filter( cluster=service_obj.cluster ).count() == 0: ClusterInfo.objects.filter( id=service_obj.cluster.id ).delete() # 当服务被删除时,如果他所属的产品下已没有其他服务,那么应该删除产品实例 if Service.objects.filter( service__product=service_obj.service.product ).count() == 0: Product.objects.filter( product=service_obj.service.product ).delete() return None exe_action = service_controllers.get(action[0]) if exe_action: salt_obj = SaltClient() service_obj.service_status = action[1] service_obj.save() time_array = time.localtime(int(time.time())) time_style = time.strftime("%Y-%m-%d %H:%M:%S", time_array) is_success, info = salt_obj.cmd(ip, exe_action, 600) # TODO 服务状态维护问题,临时解决方案,休眠保持中间态 if need_sleep: time.sleep(35) service_obj.service_status = action[2] if is_success else action[3] service_obj.save() logger.info(f"执行 [{action[0]}] 操作 {is_success},原因: {info}") ServiceHistory.objects.create( username=operation_user, description=f"执行 [{action[0]}] 操作", result=result_json.get(is_success), created=time_style, service=service_obj ) logger.info(f"服务操作详情:{info}") return ip, info else: logger.error(f"数据库无{action[0]}动作") raise ValueError(f"数据库无{action[0]}动作") @shared_task def clear_db(task_id): """ # ToDo 懒得写了以后优化 """ days_ago = timezone.now() - timezone.timedelta( days=CLEAR_DB.get('health').get("day", 7) ) SelfHealingHistory.objects.filter(end_time__lt=days_ago).delete() days_ago = timezone.now() - timezone.timedelta( days=CLEAR_DB.get('alert').get("day", 7) ) Alert.objects.filter(create_time__lt=days_ago).delete() ================================================ FILE: omp_server/services/urls.py ================================================ from django.urls import path from rest_framework.routers import DefaultRouter from services.views import ( ServiceListView, ServiceDetailView, ServiceActionView, ServiceDeleteView, ServiceStatusView, ServiceDataJsonView, AppListView, AppConfCheckView, ) from services.self_heal_view import ( SelfHealingSettingView, ListSelfHealingHistoryView, UpdateSelfHealingHistoryView ) router = DefaultRouter() router.register("services", ServiceListView, basename="services") router.register("services", ServiceDetailView, basename="services") router.register("action", ServiceActionView, basename="action") router.register("delete", ServiceDeleteView, basename="delete") router.register("SelfHealingSetting", SelfHealingSettingView, basename="SelfHealingSetting") router.register("ListSelfHealingHistory", ListSelfHealingHistoryView, basename="ListSelfHealingHistory") router.register("UpdateSelfHealingHistory", UpdateSelfHealingHistoryView, basename="UpdateSelfHealingHistory") router.register("serviceStatus", ServiceStatusView, basename="serviceStatus") # Accept_manager router.register("appList", AppListView, basename="appList") router.register("appConfCheck", AppConfCheckView, basename="appConfCheck") urlpatterns = [ path('data_json', ServiceDataJsonView.as_view(), name="serviceDataJson") ] urlpatterns += router.urls ================================================ FILE: omp_server/services/views.py ================================================ import json import logging import os from django.conf import settings from django.http import Http404 from django_filters.rest_framework.backends import DjangoFilterBackend from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ( ListModelMixin, RetrieveModelMixin, CreateModelMixin ) from rest_framework.response import Response from rest_framework.filters import OrderingFilter from db_models.models import Service, ApplicationHub, MainInstallHistory, Host from utils.parse_config import BASIC_ORDER from service_upgrade.update_data_json import DataJsonUpdate from services.permission import GetDataJsonAuthenticated from services.tasks import exec_action from services.services_filters import ServiceFilter from services.services_serializers import ( ServiceSerializer, ServiceDetailSerializer, ServiceActionSerializer, ServiceDeleteSerializer, ServiceStatusSerializer, AppListSerializer ) from promemonitor.prometheus import Prometheus from promemonitor.grafana_url import explain_url from utils.common.exceptions import OperateError from utils.common.paginations import PageNumberPager from operator import itemgetter from services.app_check import ( ConfCheck, ManagerService ) logger = logging.getLogger('server') class ServiceListView(GenericViewSet, ListModelMixin): """ list: 查询服务列表 """ queryset = Service.objects.all() serializer_class = ServiceSerializer pagination_class = PageNumberPager # 过滤,排序字段 filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = ServiceFilter ordering_fields = ("ip", "service_instance_name") # 动态排序字段 dynamic_fields = ("cpu_usage", "mem_usage") # 操作描述信息 get_description = "查询服务列表" def list(self, request, *args, **kwargs): # 获取序列化数据列表 queryset = self.filter_queryset(self.get_queryset()) real_query = queryset # 实时获取服务动态git prometheus_obj = Prometheus() is_success, prometheus_dict = prometheus_obj.get_all_service_status() # 当未指定排序字段且查询成功时 query_field = request.query_params.get("ordering", "") if is_success and query_field == "": stop_ls = [] natural_ls = [] no_monitor_ls = [] ing_ls = [] for service in queryset: # 当服务状态为 '正常' 和 '异常' 时 if service.service_status in (Service.SERVICE_STATUS_NORMAL, Service.SERVICE_STATUS_STOP): key_name = f"{service.ip}_{service.service_instance_name}" status = prometheus_dict.get(key_name, None) if status is None: no_monitor_ls.append(service) elif not status: stop_ls.append(service) else: natural_ls.append(service) else: ing_ls.append(service) real_query = stop_ls + ing_ls + natural_ls + no_monitor_ls serializer = self.get_serializer( self.paginate_queryset(real_query), many=True) serializer_data = serializer.data # 若获取成功,则动态覆盖服务状态 if is_success: status_dict = { True: "正常", False: "停止", None: "未监控", } for service_obj in serializer_data: # 如果服务状态为 '正常' 和 '停止' 的服务,通过 Prometheus 动态更新 if service_obj.get("service_status") in ("正常", "停止"): # 如果是 web 服务,则状态直接置为正常 if service_obj.get("is_web"): service_obj["service_status"] = "正常" continue key_name = f"{service_obj.get('ip')}_{service_obj.get('service_instance_name')}" status = prometheus_dict.get(key_name, None) service_obj["service_status"] = status_dict.get(status) # 获取监控及日志的url serializer_data = explain_url( serializer_data, is_service=True) serializer_data = prometheus_obj.get_service_info(serializer_data) reverse_flag = False if query_field.startswith("-"): reverse_flag = True query_field = query_field[1:] # 若排序字段在类视图 dynamic_fields 中,则对根据动态数据进行排序 none_ls = list(filter( lambda x: x.get(query_field) is None, serializer_data)) exists_ls = list(filter( lambda x: x.get(query_field) is not None, serializer_data)) if query_field in self.dynamic_fields: exists_ls = sorted( exists_ls, key=lambda x: x.get(query_field), reverse=reverse_flag) exists_ls.extend(none_ls) return self.get_paginated_response(exists_ls) class ServiceDetailView(GenericViewSet, RetrieveModelMixin): """ read: 查询服务详情 """ queryset = Service.objects.all() serializer_class = ServiceDetailSerializer # 操作描述信息 get_description = "查询服务详情" class ServiceActionView(GenericViewSet, CreateModelMixin): """ create: 服务启停删除 """ queryset = Service.objects.all() serializer_class = ServiceActionSerializer post_description = "执行启动停止或卸载操作" def create(self, request, *args, **kwargs): many_data = self.request.data.get('data') for data in many_data: action = data.get("action") instance = data.get("id") operation_user = data.get("operation_user") del_file = data.get("del_file", True) service_obj = Service.objects.filter(id=instance).first() need_split = ["hadoop"] if service_obj and service_obj.service.app_name in need_split and action == 4: delete_objs = Service.objects.filter(ip=service_obj.ip, service__app_name="hadoop") status = service_obj.service_status delete_objs.update(service_status=Service.SERVICE_STATUS_DELETING) if status != Service.SERVICE_STATUS_DELETING \ and delete_objs.first().id == service_obj.id: del_file = True else: del_file = False if action and instance and operation_user: if action == 4: try: service_obj.service_status = Service.SERVICE_STATUS_DELETING service_obj.save() except Exception as e: logger.error(f"service实例id,不存在{instance}:{e}") return Response("执行异常") exec_action.delay(action, instance, operation_user, del_file) else: raise OperateError("请输入action或id") return Response("执行成功") class ServiceDeleteView(GenericViewSet, CreateModelMixin): """ create: 服务删除校验 """ queryset = Service.objects.all() serializer_class = ServiceDeleteSerializer post_description = "查看服务删除校验依赖" def create(self, request, *args, **kwargs): """ 检查被依赖关系,包含多服务匹配 例如 jdk-1.8和 test-app被同时标记删除 test-app依赖jdk-1.8,同时标记则不显示依赖。单选jdk1.8则会显示。 """ many_data = self.request.data.get('data') service_objs = Service.objects.all() app_objs = ApplicationHub.objects.all() service_json = {} dependence_dict = [] # 存在的service key for i in service_objs: service_key = f"{i.service.app_name}-{i.service.app_version}" service_json[i.id] = service_key # 全量app的dependence反向 for app in app_objs: if app.app_dependence: for i in json.loads(app.app_dependence): dependence_dict.append( {f"{i.get('name')}-{i.get('version')}": f"{app.app_name}-{app.app_version}"} ) exist_service = set() # 过滤存在的实例所属app的key for data in many_data: instance = int(data.get("id")) filter_list = service_json.get(instance) exist_service.add(filter_list) # 查看存在的服务有没有被依赖的,做set去重 res = set() for i in exist_service: for j in dependence_dict: if j.get(i): res.add(j.get(i)) res = res - exist_service # 查看是否需要被依赖的是否已不存在 res = res & set(service_json.values()) res = "存在依赖信息:" + ",".join(res) if res else "无依赖信息" return Response(res) class ServiceStatusView(GenericViewSet, ListModelMixin): """ list: 查询服务列表 """ queryset = Service.objects.filter( service__is_base_env=False) serializer_class = ServiceStatusSerializer authentication_classes = () permission_classes = () # 操作描述信息 get_description = "查询服务状态" def list(self, request, *args, **kwargs): # 获取序列化数据列表 queryset = self.get_queryset() real_query = queryset # 实时获取服务动态git prometheus_obj = Prometheus() is_success, prometheus_dict = prometheus_obj.get_all_service_status() if is_success: stop_ls = [] natural_ls = [] no_monitor_ls = [] ing_ls = [] for service in queryset: # 当服务状态为 '正常' 和 '异常' 时 if service.service_status in (Service.SERVICE_STATUS_NORMAL, Service.SERVICE_STATUS_STOP): key_name = f"{service.ip}_{service.service_instance_name}" status = prometheus_dict.get(key_name, None) if status is None: no_monitor_ls.append(service) elif not status: stop_ls.append(service) else: natural_ls.append(service) else: ing_ls.append(service) real_query = stop_ls + ing_ls + natural_ls + no_monitor_ls serializer = self.get_serializer(real_query, many=True) serializer_data = serializer.data # 若获取成功,则动态覆盖服务状态 if is_success: for service_obj in serializer_data: # 如果服务状态为 '正常' 和 '停止' 的服务,通过 Prometheus 动态更新 if service_obj.get("service_status") in ("正常", "停止"): # 如果是 web 服务,则状态直接置为正常 if service_obj.get("is_web"): service_obj["service_status"] = True continue key_name = f"{service_obj.get('ip')}_{service_obj.get('service_instance_name')}" status = prometheus_dict.get(key_name, None) service_obj["service_status"] = status return Response(serializer_data) class ServiceDataJsonView(APIView): # for automated testing permission_classes = (GetDataJsonAuthenticated,) def get(self, request): main_install = MainInstallHistory.objects.order_by("-id").first() if not main_install: raise Http404('No install history matches the given query.') json_path = os.path.join( settings.PROJECT_DIR, f"package_hub/data_files/{main_install.operation_uuid}.json" ) if not os.path.exists(json_path): DataJsonUpdate(main_install.operation_uuid).create_json_file() with open(json_path, "r") as f: json_data = json.load(f) return Response({"json_data": json_data}) class AppListView(GenericViewSet, ListModelMixin, CreateModelMixin): queryset = ApplicationHub.objects.all().exclude(app_name__in=["hadoop", "doim"]) serializer_class = AppListSerializer get_description = "应用列表查询" post_description = "列表合法性校验" need_key = ['base_dir', 'run_user', 'service_port', 'data_dir', 'log_dir'] def list(self, request, *args, **kwargs): queryset = self.get_queryset() app_dc = {} app_ls = [] for query in queryset: if query.pro_info: pro_name = query.pro_info["pro_name"] pro_version = query.pro_info["pro_version"] app_dc.setdefault(pro_name, {}) app_dc[pro_name].setdefault("version", []) if pro_version not in app_dc[pro_name]["version"]: app_dc[pro_name]["version"].append(pro_version) app_dc[pro_name].setdefault("child", {}) app_dc[pro_name]["child"].setdefault(pro_version, []).append( {"name": query.app_name, "version": query.app_version}) else: app_dc.setdefault(query.app_name, {}) app_dc[query.app_name].setdefault("version", []).append(query.app_version) basic_ls = [list() for _ in range(len(BASIC_ORDER))] pro_ls = [] for name, info in app_dc.items(): tmp_dc = { "name": name, "version": info["version"] } if info.get("child"): tmp_dc.update({"child": info.get("child")}) pro_ls.append(tmp_dc) else: for index, name_ls in BASIC_ORDER.items(): if name in name_ls: basic_ls[index].append(tmp_dc) for i in basic_ls: app_ls.extend(i) app_ls.extend(pro_ls) return Response(app_ls) def check_repeat(self, app_data): """ 查询重复 """ for info in app_data: if len(info.get("version", [])) != 1: info.setdefault("error", f"{info.get('name')}不允许纳管不同版本") self.error = True if info.get("child"): app_names = [] for app_info in list(info["child"].values())[0]: app_name = app_info.get("name") if app_name not in app_names: app_names.append(app_name) else: info.setdefault("error", f"{info.get('name')}产品下{app_name}服务只允许选其中一个") self.error = True @staticmethod def check_dependence_one(dependence_dc, installed_ser_all): """ 检查单个服务依赖是否存在 """ lost_ser = [] for dependence in dependence_dc: dependence_ser = f'{dependence.get("name")}:{dependence.get("version")}' if dependence_ser not in installed_ser_all: lost_ser.append(dependence_ser) return lost_ser def check_dependence(self, dependence_dc, app_dc, app_data): """ 查询依赖初次筛选。仅对照应用商店实例,不对照具体实例依赖,并追加参数 """ if self.error: return # 查询当前所有服务的name:version,过滤状态异常的服务 ser_name_version = Service.objects.filter(service_status__in=range(0, 5)).values_list( "service__app_name", "service__app_version") installed_ser = [] for ser in ser_name_version: installed_ser.append(f"{ser[0]}:{ser[1].split('.')[0]}") for info in app_data: lost_ser_set = set() if info.get("child") and list(info["child"].values())[0]: for app in list(info["child"].values())[0]: app_key = f"{app['name']}:{app['version']}" lost_ser = self.check_dependence_one(dependence_dc.get(app_key), installed_ser) lost_ser_set.update(set(lost_ser)) has_install_app_args = dict(zip(self.need_key, app_dc.get(app_key))) if app_dc.get( app_key) else dict(zip(self.need_key, [""] * len(self.need_key))) app.update(has_install_app_args) else: app_key = f'{info.get("name", "")}:{info.get("version", ["0"])[0]}' lost_ser = self.check_dependence_one(dependence_dc.get(app_key), installed_ser) lost_ser_set.update(set(lost_ser)) has_install_app_args = dict(zip(self.need_key, app_dc.get(app_key))) if app_dc.get( app_key) else dict(zip(self.need_key, [""] * len(self.need_key))) info.update(has_install_app_args) if lost_ser_set: info.update({"error": f'缺失依赖:{",".join(list(lost_ser_set))}'}) self.error = True @staticmethod def explain_json(text): if text: return json.loads(text) return [] @classmethod def gets_app_list(cls): """ 获取app列表信息 """ app_ls = ApplicationHub.objects.all(). \ values_list("app_name", "app_version", "app_install_args", "app_port", "app_dependence") app_dc = {} dependence_dc = {} for app in app_ls: app_install_port = {} for install in cls.explain_json(app[2]): app_install_port[install.get('key')] = install.get('default') for port in cls.explain_json(app[3]): app_install_port[port.get('key')] = port.get('default') for key in cls.need_key: app_install_port.setdefault(key, "") app_dc[f"{app[0]}:{app[1]}"] = itemgetter(*cls.need_key)(app_install_port) dependence_dc[f"{app[0]}:{app[1]}"] = cls.explain_json(app[4]) return app_dc, dependence_dc def create(self, request, *args, **kwargs): if not hasattr(self, "error"): setattr(self, "error", False) app_data = self.request.data.get("data", []) # 查询是否勾选想通服务多版本情况 self.check_repeat(app_data) # 查询安装参数及依赖 app_dc, dependence_dc = self.gets_app_list() # 检查依赖是否存在 self.check_dependence(dependence_dc, app_dc, app_data) res_dc = {"service": app_data, "is_continue": False if self.error else True} hosts_info = Host.objects.filter( host_agent=Host.AGENT_RUNNING).values_list("ip", "agent_dir") res_dc["ips"] = dict(hosts_info) return Response(res_dc) class AppConfCheckView(GenericViewSet, CreateModelMixin): queryset = ApplicationHub.objects.all() serializer_class = AppListSerializer post_description = "校验列表问题" def create(self, request, *args, **kwargs): """ 传uuid和不传uuid """ app_data = self.request.data.get("data") if not app_data.get("uuid"): if not app_data.pop("is_continue"): raise OperateError("存在不允许继续纳管的服务") app_data = ConfCheck(app_data).run() else: app_data = ManagerService(app_data).run() return Response(app_data) ================================================ FILE: omp_server/tests/__init__.py ================================================ ================================================ FILE: omp_server/tests/base.py ================================================ import json from django.test import TestCase from rest_framework.reverse import reverse from db_models.models import UserProfile, Env class BaseTest(TestCase): """ 测试基础类 """ def setUp(self): self.login_url = reverse("login") # 创建默认用户 self.default_user = self.create_default_user() self.default_env = self.create_default_env() def get(self, url, data=None): return self.client.get( url, data=data if data else None, ) def post(self, url, data): return self.client.post( url, data=json.dumps(data), content_type="application/json; charset=utf-8", ) def delete(self, url, data=None): return self.client.delete( url, data=json.dumps(data) if data else None, content_type="application/json; charset=utf-8", ) def put(self, url, data): return self.client.put( url, data=json.dumps(data), content_type="application/json; charset=utf-8", ) def patch(self, url, data): return self.client.patch( url, data=json.dumps(data), content_type="application/json; charset=utf-8", ) def login(self, remember=False): """ 登录,签发 token 令牌 """ login_data = { "username": self.default_user.username, "password": self.default_user.password, } if remember: login_data["remember"] = True resp = self.post(self.login_url, login_data) return resp def logout(self): """ 退出登录,清除 cookies 中的 token 令牌 """ self.client.cookies.pop("jwtToken") @staticmethod def create_default_user(): """ 创建默认用户 """ queryset = UserProfile.objects.filter(username="admin") if queryset.exists(): return queryset.first() user_obj = UserProfile.objects.create_user( username="admin", password="adminPassword", email="admin@cloudwise.com", ) user_obj.password = "adminPassword" return user_obj @staticmethod def create_default_env(): """ 创建默认环境 """ queryset = Env.objects.filter(id=1) if queryset.exists(): return return Env.objects.create(id=1, name="default") class AutoLoginTest(BaseTest): """ 自动登录测试基类 """ def setUp(self): super(AutoLoginTest, self).setUp() self.login() def tearDown(self): super(AutoLoginTest, self).tearDown() self.logout() ================================================ FILE: omp_server/tests/mixin.py ================================================ """ 单元测试资源模拟混入类 """ import time import json import random from db_models.models import ( Host, Env, Labels, Service, ServiceHistory, GrafanaMainPage, ClusterInfo, ApplicationHub, ProductHub, UploadPackageHistory, MainInstallHistory, DetailInstallHistory ) from utils.plugin.crypto import AESCryptor class HostsResourceMixin: """ 主机资源混入类 """ INSTANCE_NAME_START = "t_host" IP_START = "127" def get_hosts(self, number=20, env_id=None): """ 获取主机 :param number: 创建数量 :param env_id: 环境实例 """ # 获取环境信息 env = None if env_id: env = Env.objects.filter(id=env_id).first() if not env: env = Env.objects.filter(id=1).first() # 创建主机 aes_crypto = AESCryptor() host_ls = [] agent_status_ls = list(map(lambda x: x[0], Host.AGENT_STATUS_CHOICES)) init_status_ls = list(map(lambda x: x[0], Host.INIT_STATUS_CHOICES)) for index in range(number): index += 1 host_ls.append(Host( instance_name=f"{self.INSTANCE_NAME_START}_{index}", ip=f"{self.IP_START}.0.0.{index}", port=36000, username=f"root{index}", password=aes_crypto.encode(f"password_{index}"), data_folder="/data", operate_system="CentOS", env=env, service_num=random.randint(0, 100), alert_num=random.randint(0, 100), host_agent=random.choice(agent_status_ls), monitor_agent=random.choice(agent_status_ls), init_status=random.choice(init_status_ls), )) Host.objects.bulk_create(host_ls) return Host.objects.filter( instance_name__startswith=self.INSTANCE_NAME_START) def destroy_hosts(self): """ 销毁主机 """ Host.objects.filter( instance_name__startswith=self.INSTANCE_NAME_START).delete() class HostBatchRequestMixin: """ 主机批量请求混入类 """ @staticmethod def get_host_batch_request(number, row=False): """ 模拟请求信息 """ host_list = [] for i in range(number): data = { "instance_name": f"host_new_{i}", "ip": f"10.10.10.{i}", "port": 36000, "username": "root", "password": "root_password", "data_folder": "/data", "operate_system": random.choice(("CentOS", "RedHat")) } if row: data["row"] = i + 1 host_list.append(data) return {"host_list": host_list} class GrafanaMainPageResourceMixin: """ Grafana 主面板资源混入类 """ INSTANCE_NAME_TUPLE = ("node", "service", "log", "mysql") INSTANCE_URL_CONTAIN = "t_grafana" def get_grafana_main_pages(self): """ 获取面板信息 """ grafana_main_page_ls = [] for instance_name in self.INSTANCE_NAME_TUPLE: grafana_main_page_ls.append(GrafanaMainPage( instance_name=instance_name, instance_url=f"/proxy/v1/{self.INSTANCE_URL_CONTAIN}/d/{instance_name}-url" )) GrafanaMainPage.objects.bulk_create(grafana_main_page_ls) return GrafanaMainPage.objects.filter( instance_name__in=self.INSTANCE_NAME_TUPLE, instance_url__contains=self.INSTANCE_URL_CONTAIN) def destroy_grafana_main_pages(self): """ 销毁面板信息 """ GrafanaMainPage.objects.filter( instance_name__in=self.INSTANCE_NAME_TUPLE, instance_url__contains=self.INSTANCE_URL_CONTAIN).delete() class LabelsResourceMixin: """ 标签资源混入类 """ LABEL_NAME_START = "t_label" def get_labels(self, number=10, label_type=None): """ 获取标签 :param number: 创建数量 :param label_type: 类型 """ label_ls = [] for index in range(number): if label_type is None: label_type = random.choice( Labels.LABELS_CHOICES)[0] index += 1 label_ls.append(Labels( label_type=label_type, label_name=f"{self.LABEL_NAME_START}_{index}" )) Labels.objects.bulk_create(label_ls) return Labels.objects.filter( label_name__startswith=self.LABEL_NAME_START) def destroy_labels(self): """ 销毁标签 """ Labels.objects.filter( label_name__startswith=self.LABEL_NAME_START).delete() class UploadPackageHistoryMixin: """ 上传安装包记录资源混入类 """ PACKAGE_NAME_START = "t_pkg" def get_upload_package_history(self, number=20, is_many=True): """ 获取上传安装包记录 :param number: 创建数量 :param is_many: 是否单次多个上传 """ history_ls = [] for index in range(number): index += 1 # 短暂休眠,避免毫秒级时间戳重复 time.sleep(0.01) opera_uuid = str(int(round(time.time() * 1000))) # 模拟单次多个安装包数量 pkg_number = 1 if is_many: pkg_number = random.randint(3, 5) for package_number in range(pkg_number): package_number += 1 history_ls.append(UploadPackageHistory( operation_uuid=opera_uuid, operation_user="admin", package_name=f"{self.PACKAGE_NAME_START}_{index}_{package_number}", package_md5=f"{self.PACKAGE_NAME_START}_{index}_{package_number}_md5", package_path=f"/data/app/{package_number}" )) UploadPackageHistory.objects.bulk_create(history_ls) return UploadPackageHistory.objects.filter( package_name__startswith=self.PACKAGE_NAME_START) def destroy_upload_package_history(self): """ 销毁上传安装包记录 """ UploadPackageHistory.objects.filter( package_name__startswith=self.PACKAGE_NAME_START).delete() class ApplicationResourceMixin(LabelsResourceMixin, UploadPackageHistoryMixin): """ 应用资源混入类 """ APP_NAME_START = "t_app" def _mock_install_info(self, index): """ 模拟应用安装信息 """ install_info = [] install_key_ls = ("base_dir", "log_dir", "data_dir", "username", "password") for key in install_key_ls: default = f"/data/app/{self.APP_NAME_START}{index}" if key == "username": default = "root" if key == "password": default = "rootPassword" install_info.append({ "name": "xxx", "key": key, "default": default, }) return json.dumps(install_info) def _create_application(self, index, is_release, app_type, label_ls, app_package, app_version): """ 创建应用 """ if app_type is None: app_type = random.choice( ApplicationHub.APP_TYPE_CHOICES)[0] if is_release is None: is_release = random.choice((True, False)) # 随机模拟冗余字段 extend_fields = { "base_env": random.choice(( True, False, "True", "False" )), "affinity": random.choice(( "", "tengine" )) } app_obj = ApplicationHub( is_release=is_release, app_type=app_type, app_name=f"{self.APP_NAME_START}_{index}", app_version=app_version, app_description="应用描述,省略一万字...", app_logo="app log svg data...", app_install_args=self._mock_install_info(index), extend_fields=extend_fields, app_package=app_package, # is_base_env=random.choice((True, False)), is_base_env=False, ) app_obj.save() # 随机模拟属于多种标签情况 label_obj_ls = random.sample( list(label_ls), random.randint(1, 2)) for label in label_obj_ls: app_obj.app_labels.add(label.id) app_obj.save() return app_obj def get_application(self, number=20, app_type=None, is_release=None): """ 获取应用 :param number: 创建数量 :param app_type: 类型 :param is_release: 是否发布 """ label_ls = self.get_labels( label_type=Labels.LABEL_TYPE_COMPONENT) # 创建上传包记录 upload_history_ls = self.get_upload_package_history( number=number, is_many=False) for index in range(number): app_package = upload_history_ls[index] index += 1 self._create_application( index, is_release, app_type, label_ls, app_package=app_package, app_version="1.0") # 随机模拟多个版本情况 random_app_ls = random.sample( list(range(number)), random.randint(0, number // 2 + 1)) # 创建上传包记录 upload_history_ls = self.get_upload_package_history( number=len(random_app_ls), is_many=False) for index in random_app_ls: app_package = upload_history_ls[index] index += 1 self._create_application( index, is_release, app_type, label_ls, app_package=app_package, app_version="2.0") return ApplicationHub.objects.filter( app_name__startswith=self.APP_NAME_START) def destroy_application(self): """ 销毁应用 """ ApplicationHub.objects.filter( app_name__startswith=self.APP_NAME_START).delete() self.destroy_upload_package_history() self.destroy_labels() class ProductResourceMixin(LabelsResourceMixin, UploadPackageHistoryMixin): """ 产品资源混入类 """ PRO_NAME_START = "t_pro" def _create_product(self, index, is_release, label_ls, pro_package, pro_version): if is_release is None: is_release = random.choice((True, False)) pro_obj = ProductHub( is_release=is_release, pro_name=f"{self.PRO_NAME_START}_{index}", pro_version=pro_version, pro_description="产品描述,省略一万字...", pro_logo="pro log svg data", pro_package=pro_package, ) pro_obj.save() # 随机模拟属于多种标签情况 label_obj_ls = random.sample( list(label_ls), random.randint(1, 2)) for label in label_obj_ls: pro_obj.pro_labels.add(label.id) pro_obj.save() return pro_obj def get_product(self, number=20, is_release=None): """ 获取产品 :param number: 创建数量 :param is_release: 是否发布 """ label_ls = self.get_labels( label_type=Labels.LABEL_TYPE_COMPONENT) # 创建上传包记录 upload_history_ls = self.get_upload_package_history( number=number, is_many=False) for index in range(number): pro_package = upload_history_ls[index] index += 1 self._create_product( index, is_release, label_ls, pro_package=pro_package, pro_version="1.0") # 随机模拟多个版本情况 random_pro_ls = random.sample( list(range(number)), random.randint(0, number // 2 + 1)) # 创建上传包记录 upload_history_ls = self.get_upload_package_history( number=len(random_pro_ls), is_many=False) for index in random_pro_ls: pro_package = upload_history_ls[index] index += 1 self._create_product( index, is_release, label_ls, pro_package=pro_package, pro_version="2.0") return ProductHub.objects.filter( pro_name__startswith=self.PRO_NAME_START) def destroy_product(self): """ 销毁应用 """ ProductHub.objects.filter( pro_name__startswith=self.PRO_NAME_START).delete() self.destroy_labels() class ClusterResourceMixin: """ 集群资源混入类 """ NAME_START = "t_cluster" def get_cluster(self, number=5, service_name="test_service"): """ 获取集群 :param number: 创建数量 :param service_name: 集群所属服务 """ cluster_type_ls = ("单实例", "主从", "哨兵", "集群") cluster_ls = [] for index in range(number): index += 1 cluster_ls.append(ClusterInfo( cluster_service_name=service_name, cluster_name=f"{self.NAME_START}_{index}", cluster_type=random.choice(cluster_type_ls), )) ClusterInfo.objects.bulk_create(cluster_ls) return ClusterInfo.objects.filter( cluster_name__startswith=self.NAME_START) def destroy_cluster(self): """ 销毁集群 """ ClusterInfo.objects.filter( cluster_name__startswith=self.NAME_START).delete() class ServicesResourceMixin(HostsResourceMixin, ClusterResourceMixin, ApplicationResourceMixin, ProductResourceMixin): """ 服务资源混入类 """ INSTANCE_NAME_START = "t_service" def get_services(self, number=20, env_id=None): """ 获取服务 :param number: 创建数量 :param env_id: 环境实例 """ # 创建主机、应用、集群 host_ls = self.get_hosts(env_id=env_id) app_ls = self.get_application(is_release=True) cluster_ls = self.get_cluster() # 获取环境信息 env = None if env_id: env = Env.objects.filter(id=env_id).first() if not env: env = Env.objects.filter(id=1).first() # 创建服务 service_ls = [] for index in range(number): index += 1 # 随机构造端口字段 service_port_ls = [{ "key": "service_port", "default": 18080, }] for port_index in range(random.randint(0, 2)): service_port_ls.append({ "key": f"http_port_{port_index}", "default": 18090 + port_index, }) # 随机分配集群 cluster = None if random.choice((True, False)): cluster = random.choice(cluster_ls) service_ls.append(Service( ip=random.choice(host_ls).ip, service_instance_name=f"{self.INSTANCE_NAME_START}_{index}", service_port=json.dumps(service_port_ls), service_status=random.choice( Service.SERVICE_STATUS_CHOICES)[0], alert_count=random.randint(1, 100), self_healing_count=random.randint(1, 100), service=random.choice(app_ls), env=env, cluster=cluster, service_controllers={ "start": "start_path", "stop": "stop_path", "restart": "restart_path", "init": "init_path", "install": "install_path", }, )) Service.objects.bulk_create(service_ls) service_queryset = Service.objects.filter( service_instance_name__startswith=self.INSTANCE_NAME_START) # 创建服务历史记录 history_ls = [] for obj in service_queryset: history_ls.append(ServiceHistory( username="admin", description="安装实例", result="success", service=obj, )) ServiceHistory.objects.bulk_create(history_ls) return service_queryset def destroy_services(self): """ 销毁服务 """ Service.objects.filter( service_instance_name__startswith=self.INSTANCE_NAME_START).delete() self.destroy_application() self.destroy_hosts() self.destroy_cluster() class InstallHistoryResourceMixin(ServicesResourceMixin): """ 安装历史记录资源混入类 """ UUID_START = "t_main" def get_install_history(self, number=5): """ 获取安装历史记录 """ main_obj = MainInstallHistory.objects.create( operation_uuid=f"{self.UUID_START}_" f"{int(round(time.time() * 1000))}") service_ls = self.get_services(number=number) detail_ls = [] for index in range(number): service = service_ls[index] index += 1 detail_ls.append(DetailInstallHistory( service=service, main_install_history=main_obj, install_detail_args={ "name": "t_name", "install_args": [{ "key": key, "name": "安装目录", "default": "/data/t_name", "dir_key": "{data_path}", "check_msg": "success", "check_flag": True } for key in ("base_dir", "data_dir", "log_dir")] })) DetailInstallHistory.objects.bulk_create(detail_ls) detail_obj_ls = DetailInstallHistory.objects.filter( main_install_history=main_obj) return main_obj, detail_obj_ls def destroy_install_history(self): """ 销毁安装历史记录 """ DetailInstallHistory.objects.filter( main_install_history__operation_uuid__startswith=self.UUID_START).delete() MainInstallHistory.objects.filter( operation_uuid__startswith=self.UUID_START).delete() self.destroy_services() ================================================ FILE: omp_server/tests/test_app_store/__init__.py ================================================ # -*- coding:utf-8 -*- # Project: __init__.py # Author:Times.niu@yunzhihui.com # Create time: 2021/10/13 5:12 下午 ================================================ FILE: omp_server/tests/test_app_store/install_data_source.py ================================================ # -*- coding: utf-8 -*- # Project: install_data_source # Author: jon.liu@yunzhihui.com # Create time: 2021-11-25 15:47 # IDE: PyCharm # Version: 1.0 # Introduction: """ 安装过程中的数据 """ import time import json import random import string from db_models.models import ( Host, ProductHub, ApplicationHub, UploadPackageHistory ) def create_host(ip="127.0.0.1"): """ 创建主机对象 :param ip: ip地址 :return: """ host_dic = { "instance_name": f"host-{ip}", "ip": ip, "port": 22, "username": "root", "password": "fake_password", "data_folder": "/test", "operate_system": "CentOS", "host_name": f"hostname-{ip}", "agent_dir": "/test" } host_obj = Host(**host_dic) host_obj.save() return host_obj def create_product(pro_name="test", pro_version="1.0.0"): """ 创建产品 :param pro_name: 产品名称 :param pro_version: 产品版本 :return: """ _operation_uuid = str(int(time.time())) # 创建产品安装包数据 test_pro_package = { "operation_uuid": _operation_uuid, "operation_user": "admin", "package_name": f"{pro_name}-{pro_version}-20211111-install.tar.gz", "package_md5": ''.join( random.sample(string.ascii_letters + string.digits, 32)), "package_path": "verified", "package_status": 3, "error_msg": None, "package_parent_id": None, "is_deleted": 0 } package_obj = UploadPackageHistory(**test_pro_package) package_obj.save() # 创建产品数据 test_pro_dic = { 'is_release': True, 'pro_name': pro_name, 'pro_version': pro_version, 'pro_description': pro_name, 'pro_dependence': None, 'pro_services': json.dumps([ {"name": "ser1", "version": pro_version}, {"name": "ser2", "version": pro_version}, ]), 'pro_logo': None, 'extend_fields': {}, 'pro_package_id': package_obj.id } test_product_obj = ProductHub(**test_pro_dic) test_product_obj.save() create_service(pro_obj=test_product_obj) def create_service_package(pro_obj, ser_name, ser_version): """ 创建服务包 :param pro_obj: :param ser_name: :param ser_version: :return: """ ser_pack_dic = { "operation_uuid": pro_obj.pro_package.operation_uuid, "operation_user": "admin", "package_name": f"{ser_name}-{ser_version}-2021111-ae8557f.tar.gz", "package_md5": ''.join( random.sample(string.ascii_letters + string.digits, 32)), "package_path": f"verified/{pro_obj.pro_name}-{pro_obj.pro_version}", "package_parent_id": pro_obj.pro_package.id, "package_status": 0, "error_msg": None, "is_deleted": 0 } ser_pack_obj = UploadPackageHistory(**ser_pack_dic) ser_pack_obj.save() return ser_pack_obj def create_service(pro_obj): """ 创建服务 :param pro_obj: :return: """ ser_lst = json.loads(pro_obj.pro_services) for item in ser_lst: ser_name = item["name"] ser_version = item["version"] ser_pack_obj = create_service_package( pro_obj=pro_obj, ser_name=ser_name, ser_version=ser_version ) ser_dic = { "is_release": True, "app_type": 1, "app_name": ser_name, "app_version": ser_version, "app_description": ser_name, "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "8080" } ]), "app_dependence": json.dumps([ { "name": "kafka", "version": "2.2.2" } ]), "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/%s" % ser_name }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/%s" % ser_name }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/%s" % ser_name }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": f"./bin/{ser_name} start", "stop": f"./bin/{ser_name} stop", "restart": f"./bin/{ser_name} restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "./scripts/bash/registerService.sh" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": "{service_port}", "process_name": "" }, "product": pro_obj, "app_package_id": ser_pack_obj.id } ser_obj = ApplicationHub(**ser_dic) ser_obj.save() ================================================ FILE: omp_server/tests/test_app_store/make_install_fake_data.py ================================================ # -*- coding: utf-8 -*- # Project: make_fake_data_for_testPro # Author: jon.liu@yunzhihui.com # Create time: 2021-11-17 14:53 # IDE: PyCharm # Version: 1.0 # Introduction: import os import json from db_models.models import ( Host, ProductHub, ApplicationHub, UploadPackageHistory ) from omp_server.settings import PROJECT_DIR # ######################### 测试应用A ################################ # 创建testPro产品安装包数据 testPro_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testPro-5.3.0-ompopen-20211111-install.tar.gz", "package_md5": "ca1601ceb5c0682a565120e0d74376f9", "package_path": "verified", "package_status": 3, "error_msg": None, "package_parent_id": None, "is_deleted": 0 } # 创建testPro产品安装包对象 testPro_package_obj = UploadPackageHistory(**testPro_package) testPro_package_obj.save() # 创建testPro下的服务安装包数据 testPro_services_packages = [ { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProApi-2.3.0-20211019113204-ae8557f.tar.gz", "package_md5": "b2efb6e605d797e29bb45fc1d7ea376d", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProSso-2.3.0-20211019113204-ae8557f.tar.tar.gz", "package_md5": "4ec22b75cb963573ef75a80289db32ee", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProDubboRpc-2.3.0-20211019113204-ae8557f.tar.gz", "package_md5": "bf2aeb1fa188303499a0ec32cc1763b6", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProAdmin-2.3.0-20211019113204-ae8557f.tar.gz", "package_md5": "49844baddab9c8e610f784c10a500612", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProZabbixApi-2.3.0-20211019113204-ae8557f.tar.gz", "package_md5": "8404acc041ae783c3798fda1b19321b9", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProWeb-2.3.0-20211018115934-5aee074.tar.gz", "package_md5": "d924cdc23c7b21eef6d33b9c2572c354", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProAdminWeb-2.3.0-20211009023221-748f711.tar.gz", "package_md5": "34f6eb46b862701c54e2a8f178afc470", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "gatewayServer-3.1.0-20211016154930-69a1a6c.tar.gz", "package_md5": "f8fa68eb4acf31294975f817d4159938", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "gatewayServerApi-3.1.0-20211016154930-69a1a6c.tar.gz", "package_md5": "9e124dd9c0eb2b0ad15e6b87f5c2c894", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "portalWeb-5.3.0-20211017051255-2e0af78.tar.gz", "package_md5": "ee1a1763e0576e2e496073b4e66fa1c4", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "portalServer-5.3.0-20211014164210-a9f3fba.tar.gz", "package_md5": "643102cc5cd6373266f9fde42c33f033", "package_path": "verified/testPro-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 } ] # 创建testPro下的服务安装包对象 for item in testPro_services_packages: item["package_parent_id"] = testPro_package_obj.id UploadPackageHistory.objects.bulk_create( [UploadPackageHistory(**el) for el in testPro_services_packages] ) # 创建 testPro ProductHub 对象数据 testPro = { 'is_release': True, 'pro_name': 'testPro', 'pro_version': '5.3.0', 'pro_description': '用户中心(Digital Operation User Center,简称 DOUC', 'pro_dependence': None, 'pro_services': json.dumps([ {"name": "testProApi", "version": "2.3.0"}, {"name": "testProSso", "version": "2.3.0"}, {"name": "testProDubboRpc", "version": "2.3.0"}, {"name": "testProAdmin", "version": "2.3.0"}, {"name": "testProZabbixApi", "version": "2.3.0"}, {"name": "testProWeb", "version": "2.3.0"}, {"name": "testProAdminWeb", "version": "2.3.0"}, {"name": "gatewayServer", "version": "3.1.0"}, {"name": "gatewayServerApi", "version": "3.1.0"}, {"name": "portalWeb", "version": "5.3.0"}, {"name": "portalServer", "version": "5.3.0"} ]), 'pro_logo': None, 'extend_fields': {}, 'pro_package_id': testPro_package_obj.id } testPro_product_obj = ProductHub(**testPro) testPro_product_obj.save() testPro_services_app = [ { "is_release": True, "app_type": 1, "app_name": "testProApi", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18241" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18241" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" }, { "name": "rocketmq", "version": "5.0" }, { "name": "redis", "version": "5.0.12" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProApi" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProApi" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProApi" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "数据库", "key": "dbname", "default": "cw_testPro" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/testProApi start", "stop": "./bin/testProApi stop", "restart": "./bin/testProApi restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "./scripts/bash/registerService.sh" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": "{service_port}", "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProSso", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18256" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18256" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProSso" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProSso" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProSso" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "数据库", "key": "dbname", "default": "cw_testPro" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/testProSso start", "stop": "./bin/testProSso stop", "restart": "./bin/testProSso restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 8, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProDubboRpc", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18246" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18247" }, { "name": "test端口", "protocol": "TCP", "key": "test_port", "default": "18247" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProDubboRpc" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProDubboRpc" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProDubboRpc" }, { "name": "启动内存", "key": "memory", "default": "2g" }, { "name": "数据库", "key": "dbname", "default": "cw_testPro" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/testProDubboRpc start", "stop": "./bin/testProDubboRpc stop", "restart": "./bin/testProDubboRpc restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 9, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProAdmin", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18266" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18266" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProAdmin" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProAdmin" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProAdmin" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "数据库", "key": "dbname", "default": "cw_testPro" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/testProAdmin start", "stop": "./bin/testProAdmin stop", "restart": "./bin/testProAdmin restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 10, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProZabbixApi", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18260" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18260" }, { "name": "rpc端口", "protocol": "TCP", "key": "rpc_port", "default": "18261" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProZabbixApi" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProZabbixApi" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProZabbixApi" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "数据库", "key": "dbname", "default": "cw_testPro" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/testProZabbixApi start", "stop": "./bin/testProZabbixApi stop", "restart": "./bin/testProZabbixApi restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 11, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProWeb", "app_version": "2.3.0", "app_description": None, "app_port": None, "app_dependence": [ { "name": "portalWeb", "version": "5.3.0" }, { "name": "tengine", "version": "1.20.1" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProWeb" } ], "app_controllers": { "start": "", "stop": "", "restart": "", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 12, "product_id": 1, "app_logo": None, "extend_fields": { "level": "1", "deploy": "", "affinity": "portalWeb", "resources": "", "auto_launch": "False", "post_action": "" }, "is_base_env": False, "app_monitor": None }, { "is_release": True, "app_type": 1, "app_name": "testProAdminWeb", "app_version": "2.3.0", "app_description": None, "app_port": None, "app_dependence": [ { "name": "portalWeb", "version": "5.3.0" }, { "name": "tengine", "version": "1.20.1" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProAdminWeb" } ], "app_controllers": { "start": "", "stop": "", "restart": "", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 13, "product_id": 1, "app_logo": None, "extend_fields": { "level": "1", "deploy": "", "affinity": "portalWeb", "resources": "", "auto_launch": "False", "post_action": "" }, "is_base_env": False, "app_monitor": None }, { "is_release": True, "app_type": 1, "app_name": "gatewayServer", "app_version": "3.1.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18201" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18202" }, { "name": "devtools端口", "protocol": "TCP", "key": "devtools_port", "default": "18203" }, { "name": "sentinel端口", "protocol": "TCP", "key": "sentinel_port", "default": "18208" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "tengine", "version": "1.20.1" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/gatewayServer" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/gatewayServer" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/gatewayServer" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/gatewayServer start", "stop": "./bin/gatewayServer stop", "restart": "./bin/gatewayServer restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 14, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "./scripts/bash/registerService.sh" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "gatewayServerApi", "app_version": "3.1.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18204" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18205" }, { "name": "devtools端口", "protocol": "TCP", "key": "devtools_port", "default": "18207" }, { "name": "sentinel端口", "protocol": "TCP", "key": "sentinel_port", "default": "18209" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "tengine", "version": "1.20.1" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/gatewayServerApi" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/gatewayServerApi" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/gatewayServerApi" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" } ], "app_controllers": { "start": "./bin/gatewayServerApi start", "stop": "./bin/gatewayServerApi stop", "restart": "./bin/gatewayServerApi restart", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 15, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "portalWeb", "app_version": "5.3.0", "app_description": None, "app_port": None, "app_dependence": None, "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/portalWeb" } ], "app_controllers": { "start": "", "stop": "", "restart": "", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 16, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "False", "post_action": "" }, "is_base_env": False, "app_monitor": None }, { "is_release": True, "app_type": 1, "app_name": "portalServer", "app_version": "5.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18206" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18206" } ], "app_dependence": [ { "name": "kafka", "version": "2.2.2" }, { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/portalServer" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/portalServer" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/portalServer" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" }, { "name": "kafka_topic名字", "key": "kafka_topic", "default": "cw-logs" }, { "name": "数据库名", "key": "dbname", "default": "cw_portal" } ], "app_controllers": { "start": "./bin/portalServer start", "stop": "./bin/portalServer stop", "restart": "./bin/portalServer restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }, "app_package_id": 17, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": { "service_port": "" }, "process_name": "" } } ] for item in testPro_services_app: item["product_id"] = testPro_product_obj.id item["app_package_id"] = UploadPackageHistory.objects.filter( package_name__startswith=f"{item['app_name']}-{item['app_version']}" ).last().id if item["app_port"]: item["app_port"] = json.dumps(item["app_port"]) if item["app_dependence"]: item["app_dependence"] = json.dumps(item["app_dependence"]) if item["app_install_args"]: item["app_install_args"] = json.dumps(item["app_install_args"]) if item["app_controllers"]: item["app_controllers"] = json.dumps(item["app_controllers"]) ApplicationHub.objects.bulk_create( [ApplicationHub(**el) for el in testPro_services_app] ) # ######################### 测试应用A ################################ # ######################### 测试应用B ################################ testProB_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProB-5.3.0-ompopen-20211111-install.tar.gz", "package_md5": "ca1601ceb5c0682a565220e0d74376f9", "package_path": "verified", "package_status": 3, "error_msg": None, "package_parent_id": None, "is_deleted": 0 } testProB_package_obj = UploadPackageHistory(**testProB_package) testProB_package_obj.save() testProB_services_packages = [ { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProBServer-2.3.0-20211019113204-ae8557f.tar.gz", "package_md5": "b2efb6e605d797f29bb45fc1d7ea376d", "package_path": "verified/testProB-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 }, { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "testProBWeb-2.3.0-20211019113204-ae8557f.tar.tar.gz", "package_md5": "4ec22b75cb9f3573ef75a80289db32ee", "package_path": "verified/testProB-5.3.0", "package_status": 0, "error_msg": None, "is_deleted": 0 } ] for item in testProB_services_packages: item["package_parent_id"] = testProB_package_obj.id UploadPackageHistory.objects.bulk_create( [UploadPackageHistory(**el) for el in testProB_services_packages] ) testProB = { 'is_release': True, 'pro_name': 'testProB', 'pro_version': '5.3.0', 'pro_description': '配置中心', 'pro_dependence': json.dumps([ { "name": "testPro", "version": "5.3.0" } ]), 'pro_services': json.dumps([ {"name": "testProBServer", "version": "2.3.0"}, {"name": "testProBWeb", "version": "2.3.0"} ]), 'pro_logo': None, 'extend_fields': {}, 'pro_package_id': testProB_package_obj.id } testProB_product_obj = ProductHub(**testProB) testProB_product_obj.save() testProB_services_app = [ { "is_release": True, "app_type": 1, "app_name": "testProBServer", "app_version": "2.3.0", "app_description": None, "app_port": [ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18341" }, { "name": "metric端口", "protocol": "TCP", "key": "metrics_port", "default": "18352" } ], "app_dependence": [ { "name": "nacos", "version": "2.0.3" }, { "name": "jdk", "version": "1.8.0" }, { "name": "mysql", "version": "5.7.34" }, { "name": "arangodb", "version": "3.6.5" }, { "name": "redis", "version": "5.0.12" } ], "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProBServer" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/testProBServer" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/testProBServer" }, { "name": "启动内存", "key": "memory", "default": "1g" }, { "name": "数据库", "key": "dbname", "default": "cw_testProB" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ], "app_controllers": { "start": "./bin/testProBServer start", "stop": "./bin/testProBServer stop", "restart": "./bin/testProBServer restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "JavaSpringBoot", "metric_port": "{service_port}", "process_name": "" } }, { "is_release": True, "app_type": 1, "app_name": "testProBWeb", "app_version": "2.3.0", "app_description": None, "app_port": None, "app_dependence": None, "app_install_args": [ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/testProBWeb" } ], "app_controllers": { "start": "", "stop": "", "restart": "", "reload": "", "install": "./scripts/install.py", "init": "" }, "app_package_id": 16, "product_id": 1, "app_logo": None, "extend_fields": { "level": "0", "deploy": "", "affinity": "", "resources": "", "auto_launch": "False", "post_action": "" }, "is_base_env": False, "app_monitor": None }, ] for item in testProB_services_app: item["product_id"] = testProB_product_obj.id item["app_package_id"] = UploadPackageHistory.objects.filter( package_name__startswith=f"{item['app_name']}-{item['app_version']}" ).last().id if item["app_port"]: item["app_port"] = json.dumps(item["app_port"]) if item["app_dependence"]: item["app_dependence"] = json.dumps(item["app_dependence"]) if item["app_install_args"]: item["app_install_args"] = json.dumps(item["app_install_args"]) if item["app_controllers"]: item["app_controllers"] = json.dumps(item["app_controllers"]) ApplicationHub.objects.bulk_create( [ApplicationHub(**el) for el in testProB_services_app] ) # ######################### 测试应用B ################################ # 公共组件部分 jdk_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "jdk-1.8.0-20211019113204-ae8557f.tar.gz", "package_md5": "b2efb6e604d797e29bb45fc1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } jdk_package_obj = UploadPackageHistory(**jdk_package) jdk_package_obj.save() jdk_app = { "is_release": True, "app_type": 0, "app_name": "jdk", "app_version": "1.8.0", "app_description": "jdk", "app_port": None, "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/jdk" } ]), "app_controllers": json.dumps({ "start": "", "stop": "", "restart": "", "reload": "", "install": "./scripts/install.py", "init": "" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "", "is_base_env": True }, "is_base_env": True, "app_monitor": { "type": "", "metric_port": "", "process_name": "" }, "app_package_id": jdk_package_obj.id } ApplicationHub.objects.create(**jdk_app) mysql_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "mysql-5.7.34-20211019113204-ae8557f.tar.gz", "package_md5": "b2efb6e605d797e29bb45fc1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } mysql_package_obj = UploadPackageHistory(**mysql_package) mysql_package_obj.save() mysql_app = { "is_release": True, "app_type": 0, "app_name": "mysql", "app_version": "5.7.34", "app_description": "mysql数据库", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18103" } ]), "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/mysql" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/mysql" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/mysql" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/mysql start", "stop": "./scripts/mysql stop", "restart": "./scripts/mysql restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": mysql_package_obj.id } ApplicationHub.objects.create(**mysql_app) nacos_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "nacos-2.0.3-20211019113204-ae8557f.tar.gz", "package_md5": "b2efb6e605d697e29bb45fc1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } nacos_package_obj = UploadPackageHistory(**nacos_package) nacos_package_obj.save() nacos_app = { "is_release": True, "app_type": 0, "app_name": "nacos", "app_version": "2.0.3", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18117" } ]), "app_dependence": json.dumps([{ "name": "jdk", "version": "1.8.0" }]), "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/nacos" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/nacos" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/nacos" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/nacos start", "stop": "./scripts/nacos stop", "restart": "./scripts/nacos restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": nacos_package_obj.id } ApplicationHub.objects.create(**nacos_app) kafka_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "kafka-2.2.2-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e605d697e29bb45fc1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } kafka_package_obj = UploadPackageHistory(**kafka_package) kafka_package_obj.save() kafka_app = { "is_release": True, "app_type": 0, "app_name": "kafka", "app_version": "2.2.2", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18217" } ]), "app_dependence": json.dumps([ { "name": "jdk", "version": "1.8.0" }, { "name": "zookeeper", "version": "1.2.2" } ]), "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/kafka" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/kafka" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/kafka" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/kafka start", "stop": "./scripts/kafka stop", "restart": "./scripts/kafka restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": kafka_package_obj.id } ApplicationHub.objects.create(**kafka_app) zookeeper_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "zookeeper-1.2.2-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e605d697f29bb45fc1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } zookeeper_package_obj = UploadPackageHistory(**zookeeper_package) zookeeper_package_obj.save() zookeeper_app = { "is_release": True, "app_type": 0, "app_name": "zookeeper", "app_version": "1.2.2", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18227" } ]), "app_dependence": json.dumps([{ "name": "jdk", "version": "1.8.0" }]), "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/zookeeper" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/zookeeper" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/zookeeper" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/zookeeper start", "stop": "./scripts/zookeeper stop", "restart": "./scripts/zookeeper restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": zookeeper_package_obj.id } ApplicationHub.objects.create(**zookeeper_app) redis_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "redis-5.0.12-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e605d697f29bb45fd1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } redis_package_obj = UploadPackageHistory(**redis_package) redis_package_obj.save() redis_app = { "is_release": True, "app_type": 0, "app_name": "redis", "app_version": "5.0.12", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18137" } ]), "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/redis" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/redis" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/redis" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/redis start", "stop": "./scripts/redis stop", "restart": "./scripts/redis restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": redis_package_obj.id } ApplicationHub.objects.create(**redis_app) rocketmq_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "rocketmq-5.0-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e605d697f29bb45fd1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } rocketmq_package_obj = UploadPackageHistory(**rocketmq_package) rocketmq_package_obj.save() rocketmq_app = { "is_release": True, "app_type": 0, "app_name": "rocketmq", "app_version": "5.0", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18147" } ]), "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/rocketmq" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/rocketmq" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/rocketmq" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/rocketmq start", "stop": "./scripts/rocketmq stop", "restart": "./scripts/rocketmq restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": rocketmq_package_obj.id } ApplicationHub.objects.create(**rocketmq_app) tengine_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "tengine-1.20.1-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e60dd697f29bb45fd1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } tengine_package_obj = UploadPackageHistory(**tengine_package) tengine_package_obj.save() tengine_app = { "is_release": True, "app_type": 0, "app_name": "tengine", "app_version": "1.20.1", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "18080" } ]), "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/tengine" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/tengine" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/tengine" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/tengine start", "stop": "./scripts/tengine stop", "restart": "./scripts/tengine restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": tengine_package_obj.id } ApplicationHub.objects.create(**tengine_app) arangodb_package = { "operation_uuid": "1636770501006", "operation_user": "admin", "package_name": "arangodb-5.6.5-20211019113204-ae8557f.tar.gz", "package_md5": "b2eb6e60dd697f29bb45fd1d7ea376d", "package_path": "verified", "package_status": 0, "error_msg": None, "is_deleted": 0 } arangodb_package_obj = UploadPackageHistory(**arangodb_package) arangodb_package_obj.save() arangodb_app = { "is_release": True, "app_type": 0, "app_name": "arangodb", "app_version": "3.6.5", "app_description": "", "app_port": json.dumps([ { "name": "服务端口", "protocol": "TCP", "key": "service_port", "default": "12345" } ]), "app_dependence": None, "app_install_args": json.dumps([ { "name": "安装目录", "key": "base_dir", "default": "{data_path}/app/arangodb" }, { "name": "日志目录", "key": "log_dir", "default": "{data_path}/logs/arangodb" }, { "name": "数据目录", "key": "data_dir", "default": "{data_path}/appData/arangodb" }, { "name": "安装用户", "key": "run_user", "default": "commonuser" } ]), "app_controllers": json.dumps({ "start": "./scripts/arangodb start", "stop": "./scripts/arangodb stop", "restart": "./scripts/arangodb restart", "reload": "", "install": "./scripts/install.py", "init": "./scripts/init.py" }), "app_logo": None, "extend_fields": { "deploy": "", "affinity": "", "resources": "", "auto_launch": "True", "post_action": "" }, "is_base_env": False, "app_monitor": { "type": "", "metric_port": "{service_port}", "process_name": "" }, "app_package_id": arangodb_package_obj.id } ApplicationHub.objects.create(**arangodb_app) # 全部安装包的创建操作 package_lst = UploadPackageHistory.objects.values( "package_path", "package_name") for item in package_lst: _path = os.path.join( PROJECT_DIR, "package_hub", item["package_path"], item["package_name"] ) if not os.path.exists(os.path.dirname(_path)): os.system(f"mkdir {os.path.dirname(_path)}") a = os.system(f"touch {_path}") hosts = [ { "is_deleted": 0, "instance_name": "127.0.0.1", "ip": "127.0.0.1", "port": 36000, "username": "root", "password": "pMMkpa5jqlJG4A-ROeMlsEHj8YvMTRpMYnNFD2YS7MA", "data_folder": "/test", "service_num": 0, "alert_num": 0, "operate_system": "CentOS", "memory": 32, "cpu": 8, "disk": {"/": 50, "/data": 47}, "host_agent": "0", "monitor_agent": "0", "is_maintenance": 0, "env_id": 1, "host_agent_error": None, "host_name": "localhost", "monitor_agent_error": None, "agent_dir": "/test" }, { "is_deleted": 0, "instance_name": "127.0.0.2", "ip": "127.0.0.2", "port": 36000, "username": "root", "password": "pMMkpa5jqlJG4A-ROeMlsEHj8YvMTRpMYnNFD2YS7MA", "data_folder": "/test", "service_num": 0, "alert_num": 0, "operate_system": "CentOS", "memory": 32, "cpu": 8, "disk": {"/": 50, "/data": 47}, "host_agent": "0", "monitor_agent": "0", "is_maintenance": 0, "env_id": 1, "host_agent_error": None, "host_name": "localhost", "monitor_agent_error": None, "agent_dir": "/test" }, { "is_deleted": 0, "instance_name": "127.0.0.3", "ip": "127.0.0.3", "port": 36000, "username": "root", "password": "pMMkpa5jqlJG4A-ROeMlsEHj8YvMTRpMYnNFD2YS7MA", "data_folder": "/test", "service_num": 0, "alert_num": 0, "operate_system": "CentOS", "memory": 32, "cpu": 8, "disk": {"/": 50, "/data": 47}, "host_agent": "0", "monitor_agent": "0", "is_maintenance": 0, "env_id": 1, "host_agent_error": None, "host_name": "localhost", "monitor_agent_error": None, "agent_dir": "/test" } ] host_lst = list() for item in hosts: host_lst.append(Host(**item)) Host.objects.bulk_create(host_lst) ================================================ FILE: omp_server/tests/test_app_store/test_app_check.py ================================================ ================================================ FILE: omp_server/tests/test_app_store/test_app_store.py ================================================ import random from django.db.models import Max from rest_framework.reverse import reverse from tests.base import AutoLoginTest from tests.mixin import ( ApplicationResourceMixin, ProductResourceMixin ) from db_models.models import ( Labels, ApplicationHub ) class LabelListTest(AutoLoginTest, ApplicationResourceMixin): """ 标签列表测试类 """ def setUp(self): super(LabelListTest, self).setUp() self.label_list_url = reverse("labels-list") self.get_application() def tearDown(self): super(LabelListTest, self).tearDown() self.destroy_application() def test_label_list(self): """ 测试标签列表 """ # 查询标签列表 -> 返回所有标签列表数据 resp = self.get(self.label_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") # 查询指定类型标签 -> 返回指定类型标签列表数据 choice = random.choice(Labels.LABELS_CHOICES) resp = self.get(self.label_list_url, { "label_type": choice[0] }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( set(resp.get("data")), set(Labels.objects.filter( label_type=choice[0], applicationhub__app_type=ApplicationHub.APP_TYPE_COMPONENT ).order_by("id").values_list("label_name", flat=True).distinct()) ) class ComponentListTest(AutoLoginTest, ApplicationResourceMixin): """ 基础组件列表测试类 """ def setUp(self): super(ComponentListTest, self).setUp() self.component_list_url = reverse("components-list") self.app_obj_ls = self.get_application() def tearDown(self): super(ComponentListTest, self).tearDown() self.destroy_application() def test_component_list_filter(self): """ 测试基础组件列表过滤 """ # 查询组件列表 -> 按名称合并展示所有已发布组件 resp = self.get(self.component_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") comp_set = set(self.app_obj_ls.filter( is_release=True, app_type=ApplicationHub.APP_TYPE_COMPONENT ).values_list("app_name")) self.assertEqual(resp.get("data").get("count"), len(comp_set)) # 组件名过滤 -> 展示组件名模糊匹配项 resp = self.get(self.component_list_url, { "app_name": "app_1" }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") comp_set = set(self.app_obj_ls.filter( is_release=True, app_type=ApplicationHub.APP_TYPE_COMPONENT, app_name__contains="app_1", ).values_list("app_name")) self.assertEqual(resp.get("data").get("count"), len(comp_set)) # 标签类型过滤 -> 展示标签名匹配项 label_name = random.choice(Labels.LABELS_CHOICES)[1] resp = self.get(self.component_list_url, { "type": label_name }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") comp_set = set(self.app_obj_ls.filter( is_release=True, app_type=ApplicationHub.APP_TYPE_COMPONENT, app_labels__label_name=label_name, ).values_list("app_name")) self.assertEqual(resp.get("data").get("count"), len(comp_set)) def test_component_list_order(self): """ 测试基础组建列表排序 """ # 查询组件列表 -> 各组件返回最新数据,按照创建时间排序 resp = self.get(self.component_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") obj_ls = list(self.app_obj_ls.filter( is_release=True, app_type=ApplicationHub.APP_TYPE_COMPONENT ).values("app_name").annotate(c=Max("created")).order_by( "-created").values_list("app_name", flat=True)) target_ls = [] for obj in obj_ls: if obj not in target_ls: target_ls.append(obj) if len(target_ls) == 10: break result_ls = list(map( lambda x: x.get("app_name"), resp.get("data").get("results"))) self.assertEqual(result_ls, target_ls) class ServiceListTest(AutoLoginTest, ProductResourceMixin): """ 产品列表测试类 """ def setUp(self): super(ServiceListTest, self).setUp() self.service_list_url = reverse("appServices-list") self.service_obj_ls = self.get_product() def tearDown(self): super(ServiceListTest, self).tearDown() self.destroy_product() def test_service_list_filter(self): """ 测试应用服务列表过滤 """ # 查询服务列表 -> 按名称合并展示所有已发布服务 resp = self.get(self.service_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") service_set = set(self.service_obj_ls.filter( is_release=True ).values_list("pro_name")) self.assertEqual(resp.get("data").get("count"), len(service_set)) # 服务名过滤 -> 展示服务名模糊匹配项 resp = self.get(self.service_list_url, { "pro_name": "pro_1" }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") service_set = set(self.service_obj_ls.filter( is_release=True, pro_name__contains="pro_1", ).values_list("pro_name")) self.assertEqual(resp.get("data").get("count"), len(service_set)) self.destroy_product() def test_service_list_order(self): """ 测试应用服务列表排序 """ # 查询应用服务列表 -> 各组件返回最新数据,按照创建时间排序 resp = self.get(self.service_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") obj_ls = list(self.service_obj_ls.filter( is_release=True, ).values("pro_name").annotate(c=Max("created")).order_by( "-created").values_list("pro_name", flat=True)) target_ls = [] for obj in obj_ls: if obj not in target_ls: target_ls.append(obj) result_ls = list(map( lambda x: x.get("pro_name"), resp.get("data").get("results"))) self.assertEqual(result_ls, target_ls[:10]) class AppStoreDetailTest(AutoLoginTest, ApplicationResourceMixin, ProductResourceMixin): """ 应用商店组件和产品测试类 """ def setUp(self): super(AppStoreDetailTest, self).setUp() self.application_detail_url = reverse("componentDetail-list") self.product_detail_url = reverse("appServiceDetail-list") def test_application_detail(self): """ 测试应用详情 """ app_ls = self.get_application() # 查询应用表 -> 返回所指定应用名符合的数据 resp = self.get(self.application_detail_url, { "app_name": random.choice(app_ls).app_name }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get('data')) self.destroy_application() def test_product_detail(self): """ 测试产品详情 """ pro_ls = self.get_product() resp = self.get(self.product_detail_url, { "pro_name": random.choice(pro_ls).pro_name }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get('data')) self.destroy_product() ================================================ FILE: omp_server/tests/test_app_store/test_app_store_install.py ================================================ # -*- coding: utf-8 -*- # Project: test_app_store_install # Author: jon.liu@yunzhihui.com # Create time: 2021-10-23 09:05 # IDE: PyCharm # Version: 1.0 # Introduction: """ 组件、应用安装入口使用的解析方法测试 """ import json import datetime import os from unittest import mock from rest_framework.reverse import reverse from rest_framework.test import APIClient from django.test import TransactionTestCase from db_models.models import ( ApplicationHub, ProductHub, UploadPackageHistory, Host, Env, UserProfile ) from omp_server.settings import PROJECT_DIR from app_store.tasks import install_service from utils.plugin.salt_client import SaltClient from tests.base import AutoLoginTest from tests.mixin import ( ApplicationResourceMixin, ProductResourceMixin ) class ComponentEntranceTest(AutoLoginTest, ApplicationResourceMixin): def setUp(self): super(ComponentEntranceTest, self).setUp() self.componentEntrance_url = reverse( "componentEntrance-list") def test_null_ret_success(self): res = self.get(self.componentEntrance_url).json() self.assertDictEqual( res, {'code': 0, 'message': 'success', 'data': []} ) def test_normal_res(self): self.get_application() res = self.get(self.componentEntrance_url).json() self.assertEqual(res.get("code"), 0) self.assertEqual(len(res.get("data", [])) != 0, True) self.destroy_application() self.destroy_labels() def make_unique_app_data(self, dic): # NOQA obj_dic = { "is_release": 1, "app_type": 0, "app_logo": "app log svg data...", "app_description": "应用描述,省略一万字...", "app_port": json.dumps( [{"default": 18080, "key": "http_port", "name": "服务端口"}] ), "app_install_args": json.dumps( [ {"name": "安装目录", "key": "install_dir", "default": "{data_path}/abc"}, {"name": "数据目录", "key": "data_dir", "default": "{data_path}/data/test_app1"} ] ) } obj_dic.update(dic) ApplicationHub(**obj_dic).save() def destroy_app(self): # NOQA ApplicationHub.objects.filter(app_name__icontains="test_app").delete() def make_base_app_1(self): # NOQA app_base_1 = { "app_name": "test_app1", "app_version": "8u211", "extend_fields": {"base_env": True} } self.make_unique_app_data(app_base_1) def make_base_app_2(self): # NOQA app_base_2 = { "app_name": "test_app2", "app_version": "1.0", "app_dependence": json.dumps( [{"name": "test_app1", "version": "8u211"}] ), "extend_fields": { "deploy": { "single": [{"key": "single", "name": "单实例"}], "complex": [{"key": "master_slave", "name": "主从模式", "nodes": {"step": 1, "start": 2}}] }, } } self.make_unique_app_data(app_base_2) def make_base_app_3(self): # NOQA app_base_3 = { "app_name": "test_app3", "app_version": "1.0", "app_dependence": json.dumps( [ {"name": "test_app2", "version": "1.0"}, {"name": "test_app4", "version": "1.0"} ] ), "extend_fields": {} } self.make_unique_app_data(app_base_3) def make_unrelease_app(self): # NOQA app_base_4 = { "app_name": "test_app4", "app_version": "1.0", "is_release": "0", "app_dependence": json.dumps( [{"name": "test_app1", "version": "1.0"}] ), "extend_fields": {} } self.make_unique_app_data(app_base_4) def test_dependence_one_level(self): """ 一层依赖信息单元测试 """ self.make_base_app_1() self.make_base_app_2() res = self.get(self.componentEntrance_url).json() self.assertEqual(res.get("code"), 0) self.assertEqual(len(res.get("data")), 2) for item in res.get("data"): if item.get("app_name") == "test_app2": self.assertEqual(len(item.get("app_dependence")), 1) self.destroy_app() def test_dependence_two_level(self): """ 二层依赖信息单元测试 """ self.make_base_app_1() self.make_base_app_2() self.make_base_app_3() res = self.get(self.componentEntrance_url).json() self.assertEqual(res.get("code"), 0) self.assertEqual(len(res.get("data")), 3) for item in res.get("data"): if item.get("app_name") == "test_app3": self.assertEqual(len(item.get("app_dependence")), 3) self.destroy_app() def test_no_release_app_dependence(self): """ 测试缺少服务依赖信息场景 """ self.make_base_app_1() self.make_base_app_2() self.make_base_app_3() self.make_unrelease_app() res = self.get(self.componentEntrance_url).json() process_continue = True for item in res.get("data"): for el in item.get("app_dependence"): if not el.get("process_continue"): process_continue = False self.assertEqual(process_continue, False) class ProductEntranceTest(ComponentEntranceTest, ProductResourceMixin): def setUp(self): super(ProductEntranceTest, self).setUp() self.productEntrance_url = reverse( "productEntrance-list") def test_normal_res(self): self.get_application() self.get_product() res = self.get(self.productEntrance_url).json() self.assertEqual(res.get("code"), 0) self.assertEqual(len(res.get("data", [])) != 0, True) self.destroy_application() self.destroy_labels() self.destroy_product() def make_pro_1(self): # NOQA """ 创建不依赖其他应用的应用,应用下具备2个服务 :return: """ # 创建产品下的服务 test_pro_ser_1 = { "app_name": "test_pro_ser_1", "app_version": "1.0", "app_dependence": json.dumps( [{"name": "test_app1", "version": "8u211"}] ) } self.make_unique_app_data(test_pro_ser_1) test_pro_ser_2 = { "app_name": "test_pro_ser_2", "app_version": "1.0", "app_dependence": json.dumps( [{"name": "test_app1", "version": "8u211"}] ) } self.make_unique_app_data(test_pro_ser_2) pro_dic = { "is_release": 1, "pro_name": "test_pro_1", "pro_version": "1.0", "pro_dependence": json.dumps([ {"name": "test_pro_2", "version": "1.0"}, {"name": "test_pro_30", "version": "1.0"}, ]), "pro_services": json.dumps([ {"name": "test_pro_ser_1", "version": "1.0"}, {"name": "test_pro_ser_2", "version": "1.0"} ]) } ProductHub(**pro_dic).save() def make_pro_2(self): # NOQA pro_dic = { "is_release": 1, "pro_name": "test_pro_2", "pro_version": "1.2", "pro_dependence": json.dumps([ {"name": "test_pro_1", "version": "1.0"}, {"name": "test_pro_2", "version": "1.0"}, ]), "pro_services": json.dumps([ {"name": "test_pro_ser_3", "version": "2.0"}, {"name": "test_pro_ser_4", "version": "2.0"}, {"name": "test_pro_ser_5", "version": "3.0"}, ]) } ProductHub(**pro_dic).save() def make_pro_3(self): # NOQA pro_dic = { "is_release": 1, "pro_name": "test_pro_3", "pro_version": "1.2", "pro_dependence": json.dumps([ {"name": "test_pro_1", "version": "1.0"}, ]), "pro_services": json.dumps([ {"name": "test_pro_ser_3", "version": "2.0"}, {"name": "test_pro_ser_4", "version": "2.0"}, {"name": "test_pro_ser_5", "version": "3.0"}, ]) } ProductHub(**pro_dic).save() def test_pro_component_dependence(self): self.make_pro_1() res = self.get(self.productEntrance_url).json() self.assertEqual(res.get("code"), 0) def test_pro_pro_dependence(self): """ 测试产品间有依赖场景 """ self.make_pro_1() self.make_pro_2() self.make_pro_3() res = self.get(self.productEntrance_url).json() self.assertEqual(res.get("code"), 0) def create_host(): """ 创建测试使用主机 :return: """ env = Env(name="default") env.save() test_host = { 'created': datetime.datetime(2021, 10, 26, 17, 59, 45, 248976), 'modified': datetime.datetime(2021, 10, 26, 17, 59, 45, 553447), 'is_deleted': False, 'instance_name': '127.0.0.1', 'ip': '127.0.0.1', 'port': 22, 'username': 'root', 'password': 'lEJBI-Pt8Ih321eaawzf1kHj8YvMTRpMYnNFD2YS7MA', 'data_folder': '/data', 'service_num': 0, 'alert_num': 0, 'operate_system': 'CentOS', 'host_name': None, 'memory': None, 'cpu': None, 'disk': None, 'host_agent': '0', 'monitor_agent': '4', 'host_agent_error': None, 'monitor_agent_error': '', 'is_maintenance': False, 'agent_dir': '/data', 'env': env } Host(**test_host).save() def create_base_app(): """ 创建安装过程中使用的app :return: """ test_app_1_upload_history = { 'created': datetime.datetime(2021, 10, 25, 10, 39, 59, 239807), 'modified': datetime.datetime(2021, 10, 25, 10, 40, 7, 191114), 'is_deleted': False, 'operation_uuid': '1635129600184', 'operation_user': 'admin', 'package_name': 'testApp-1.0.1-35144e57a59d774869ccc218539db8c7.tar.gz', 'package_md5': '35144e57a59d774869ccc218539db8c7', 'package_path': 'verified', 'package_status': 3, 'error_msg': None, 'package_parent_id': None } test_app_1_application_hub = { 'created': datetime.datetime(2021, 10, 25, 10, 40, 29, 981060), 'modified': datetime.datetime(2021, 10, 25, 10, 40, 29, 988651), 'is_release': True, 'app_type': 0, 'app_name': 'testApp', 'app_version': '1.0.1', 'app_description': 'Java Development Kit (JDK) 是Sun公司(已被Oracle收购)' '针对Java开发员的软件开发工具包' 'Java SDK(Software development kit)。', 'app_port': json.dumps([ {"default": 8080, "key": "http_port", "name": "业务端口"}, {"default": 8081, "key": "metric_port", "name": "监控端口"} ]), 'app_dependence': None, 'app_install_args': json.dumps([ {"name": "安装目录", "key": "base_dir", "default": "{data_path}/jdk"}, {"name": "数据目录", "key": "data_dir", "default": "{data_path}/data/jdk"}, {"name": "日志目录", "key": "log_dir", "default": "{data_path}/log/jdk"}, {"name": "用户名", "key": "username", "default": "jon"}, {"name": "密码", "key": "password", "default": "jon_password"}, ]), 'app_controllers': json.dumps( { "start": "./bin/testApp start", "stop": "./bin/testApp stop", "restart": "./bin/testApp restart", "reload": "./bin/testApp reload", "install": "./scripts/install.py", "init": "" } ), 'app_package_id': 1, 'product_id': None, 'app_logo': None, 'extend_fields': { 'deploy': { "single": [{"name": "单实例", "key": "single"}], "complex": [{"name": "主从模式", "key": "master_slave"}] }, 'monitor': None, 'base_env': True, 'resources': None, 'auto_launch': False } } up_obj = UploadPackageHistory(**test_app_1_upload_history) up_obj.save() test_app_1_application_hub["app_package_id"] = up_obj.id app_obj = ApplicationHub(**test_app_1_application_hub) app_obj.save() class ExecuteInstallTest(TransactionTestCase): def setUp(self): super(ExecuteInstallTest, self).setUp() self.executeInstall_url = reverse( "executeInstall-list") create_host() create_base_app() user = UserProfile.objects.create(username="admin") self.client = APIClient() self.client.force_authenticate(user) self.test_data = { "install_type": 0, "use_exist_services": [ ], "install_services": [ { "name": "testApp", "version": "1.0.1", "ip": "127.0.0.1", "install_args": [ {"name": "安装目录", "key": "base_dir", "dir_key": "{data_path}", "default": "/jdk"}, {"name": "数据目录", "key": "data_dir", "dir_key": "{data_path}", "default": "/data/jdk"}, {"name": "日志目录", "key": "log_dir", "dir_key": "{data_path}", "default": "/log/jdk"}, {"name": "用户名", "key": "username", "default": "jon"}, {"name": "密码", "key": "password", "default": "jon_password"}, ], "app_port": [ {"default": 8000, "key": "http_port", "name": "业务端口"}, {"default": 8081, "key": "metric_port", "name": "监控端口"} ], "service_instance_name": "testApp-jon", "deploy_mode": { "key": "single", "name": "单实例" } } ] } @mock.patch.object(SaltClient, "cmd", return_value=(True, "OK")) @mock.patch.object(install_service, "delay", return_value=None) @mock.patch.object(SaltClient, "cmd", return_value=(False, "OK")) # @mock.patch( # "utils.plugin.public_utils.check_ip_port", return_value=(False, "")) def test_main_success(self, *args, **kwargs): res = self.client.post( self.executeInstall_url, data=json.dumps(self.test_data), content_type="application/json" ).json() self.assertEqual(res.get("code"), 0) # operation_uuid = res.get("data", {}).get("operation_uuid") # self.assertTrue(operation_uuid) # 删除json文件 os.system(f"rm -rf {PROJECT_DIR}/package_hub/data_files/*.json") @mock.patch.object(SaltClient, "cmd", return_value=(False, "")) @mock.patch.object(install_service, "delay", return_value=None) # @mock.patch( # "utils.plugin.public_utils.check_ip_port", return_value=(False, "")) @mock.patch.object(SaltClient, "cmd", return_value=(False, "OK")) def test_failed_path_exist(self, *args, **kwargs): res = self.client.post( self.executeInstall_url, data=json.dumps(self.test_data), content_type="application/json" ).json() self.assertEqual(res.get("code"), 0) # 删除json文件 os.system(f"rm -rf {PROJECT_DIR}/package_hub/data_files/*.json") class InstallHistoryTest(ExecuteInstallTest): def setUp(self): super(InstallHistoryTest, self).setUp() self.installHistory_url = reverse( "installHistory-list") def test_success(self): self.test_main_success() res = self.client.get(self.installHistory_url).json() self.assertEqual(res.get("code"), 0) ================================================ FILE: omp_server/tests/test_app_store/test_app_store_upload.py ================================================ from rest_framework.reverse import reverse from db_models.models import ( ApplicationHub, ProductHub, UploadPackageHistory, Labels ) from tests.base import AutoLoginTest from unittest import mock from app_store.tasks import front_end_verified from utils.plugin import public_utils from app_store.tasks import ( ExplainYml, PublicAction, publish_bak_end, publish_entry, exec_clear ) from unittest.mock import patch, mock_open from app_store.tmp_exec_back_task import back_end_verified_init import os current_dir = os.path.dirname(os.path.abspath(__file__)) project_dir = os.path.dirname(os.path.dirname(os.path.dirname(current_dir))) test_product = { "kind": "product", "name": "jenkins", "version": "5.2.0", "description": "这是jenkins", "labels": ["CI&CD"], "dependencies": None, "service": [{"name": "jenkins"}], "extend_fields": {} } test_service = { "kind": "service", "name": "jenkins", "version": "2.303.2", "ports": [{"name": "服务端口", "protocol": "TCP", "key": "service_port", "default": 8080}], "dependencies": [{"name": "jdk", "version": "1.8.0"}], "install": [{"name": "安装目录", "key": "base_dir", "default": "path/jenkins"}], "control": [ { "start": "./bin/start.sh", "stop": "./bin/stop.sh", "restart": "./bin/restart.sh", "reload": "./bin/reload.sh", "install": "./scripts/install.sh", "init": "./scripts/init.sh" } ], "base_env": False, "monitor": { "process_name": "jenkins", "metric_port": "service_port", "type": "JavaSpringBoot" }, "extend_fields": { "auto_launch": True, "affinity:": None, "level": "1", "deploy": None, "resources": {"cpu": "2c", "memory": "2g"} } } service_yml = """ kind: service name: jenkins version: 2.303.2 auto_launch: True base_env: False level: 1 monitor: process_name: "jenkins" metric_port: {service_port} type: "JavaSpringBoot" ports: - name: 服务端 protocol: TCP default: 8080 key: service_port dependencies: - name: jdk version: 1.8 resources: cpu: 1000m memory: 2000m install: - name: "安装目录" key: base_dir default: "{data_path}/jeknins" affinity: deploy: control: start: "./bin/start.sh" stop: "./bin/stop.sh" restart: "./bin/restart.sh" reload: "./bin/reload.sh" install: "./scripts/install.sh" init: "./scripts/init.sh" post_action: """ component_yml = """ kind: component name: tengine version: 2.3.3 description: "服务tengine" labels: - WEB服务 auto_launch: True base_env: False monitor: process_name: "nginx" metric_port: {service_port} type: "JavaSpringBoot" ports: - name: 服务端口 protocol: TCP key: service_port default: 80 deploy: affinity: dependencies: resources: cpu: 1000m memory: 500m install: - name: "安装目录" key: base_dir default: "{data_path}/tengine" - name: "日志目录" key: log_dir default: "{data_path}/tengine/logs" - name: "vhosts" key: vhosts_dir default: "{data_path}/tengine/vhosts" control: start: "./bin/start.sh" stop: "./bin/stop.sh" restart: "./bin/restart.sh" reload: "./bin/reload.sh" install: "./scripts/install.sh" init: "./scripts/init.sh" post_action: """ product_yml = """ kind: product name: jenkins version: 5.2.0 description: "Jenkins开源" labels: - CI&CD dependencies: service: - name: jenkins """ class PackageUploadTest(AutoLoginTest): # 上传逻辑 def setUp(self): super(PackageUploadTest, self).setUp() self.upload_url = reverse( "upload-list") UploadPackageHistory( operation_uuid='test-uuid', operation_user='admin', package_name='jenkins-1.0.0-test-md5.tar.gz', package_md5='test-md5', package_path="verified" ).save() @mock.patch.object(public_utils, "local_cmd", return_value="") @mock.patch( "os.mkdir", return_value=None) @mock.patch( "os.path.exists", return_value="") @mock.patch( "app_store.tasks.ExplainYml.explain_yml", return_value="" ) @mock.patch( "os.listdir", return_value=["jenkins-2.303.2.tar.gz"] ) @mock.patch( "os.path.isfile", return_value=True ) def test_app_store_upload(self, isfile, listdir, explain, exists, mkdir, local_cmd): # 正向前端发布 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') local_cmd.side_effect = [ ("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 0), ("success", "", 0), ("test-md6 jenkins01-1.0.0.tar.gz", "", 0) ] exists.side_effect = [ True, False, True ] explain.side_effect = [(True, test_product), (True, test_service)] front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() clear_file = os.path.join( project_dir, 'data', "middle_data-test-uuid.json") os.remove(clear_file) res = upload_obj.package_status self.assertEqual(res, 0) @mock.patch.object(public_utils, "local_cmd", return_value=("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 1)) def test_app_store_upload_md5(self, local_cmd): # 反向md5校验失败 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() res = upload_obj.package_status self.assertEqual(res, 1) @mock.patch.object(public_utils, "local_cmd", return_value="") @mock.patch( "os.mkdir", return_value=None) def test_app_store_upload_tar(self, mkdir, local_cmd): # 反向tar解压失败 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') local_cmd.side_effect = [ ("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 0), ("false", "", 1) ] front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() res = upload_obj.package_status self.assertEqual(res, 1) @mock.patch.object( public_utils, "local_cmd", return_value="" ) @mock.patch( "os.path.exists", return_value=False) @mock.patch( "os.mkdir", return_value=None) def test_app_store_upload_file_check(self, mkdir, exists, local_cmd): # 产品或组建yaml文件检测文件存在 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') local_cmd.side_effect = [ ("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 0), ("false", "", 0) ] front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() res = upload_obj.package_status self.assertEqual(res, 1) @mock.patch.object( public_utils, "local_cmd", return_value="" ) @mock.patch( "os.path.exists", return_value="") @patch( "builtins.open", new_callable=mock_open, read_data="this-is-image" ) @mock.patch( "app_store.tasks.ExplainYml.explain_yml", return_value=(True, test_product) ) @mock.patch( "os.listdir", return_value=["jenkins-2.303.2.tar.gz"] ) @mock.patch( "os.path.isfile", return_value=True ) @mock.patch( "os.mkdir", return_value=None) def test_app_store_upload_file_service(self, mkdir, isfile, listdir, explain, image, exists, local_cmd): # 服务yaml文件检测文件存在 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') local_cmd.side_effect = [ ("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 0), ("false", "", 0), ("false", "", 1) ] exists.side_effect = [ True, True, False ] front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() res = upload_obj.package_status self.assertEqual(res, 1) @mock.patch.object( public_utils, "local_cmd", return_value="" ) @mock.patch( "os.path.exists", return_value="") @patch( "builtins.open", new_callable=mock_open, read_data="this-is-image" ) @mock.patch( "app_store.tasks.ExplainYml.explain_yml", return_value="" ) @mock.patch( "os.listdir", return_value=["jenkins-2.303.2.tar.gz"] ) @mock.patch( "os.path.isfile", return_value=True ) @mock.patch( "os.mkdir", return_value=None) def test_app_store_upload_file_md5(self, mkdir, isfile, listdir, explain, image, exists, local_cmd): # 服务md5sum校验失败 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') local_cmd.side_effect = [ ("test-md5 jenkins-1.0.0-test-md5.tar.gz", "", 0), ("false", "", 0) ] exists.side_effect = [ True, True, False ] explain.side_effect = [ (True, test_product), (True, test_service) ] front_end_verified(upload_obj.operation_uuid, upload_obj.operation_user, upload_obj.package_name, "RandomStr", "front_end_verified", upload_obj.id) upload_obj.refresh_from_db() res = upload_obj.package_status self.assertEqual(res, 1) @patch("builtins.open", new_callable=mock_open, read_data=service_yml) def test_app_store_explain_service(self, with_open): # 正向解析服务 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') public_action = PublicAction(upload_obj.package_md5) explain = ExplainYml(public_action, '/data/test').explain_yml() upload_obj.refresh_from_db() self.assertEqual(explain[0], True) self.assertEqual(explain[1].get('version'), "2.303.2") @patch("builtins.open", new_callable=mock_open, read_data=component_yml) def test_app_store_explain_component(self, with_open): # 正向解析组件 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') public_action = PublicAction(upload_obj.package_md5) explain = ExplainYml(public_action, '/data/test').explain_yml() upload_obj.refresh_from_db() self.assertEqual(explain[0], True) self.assertEqual(explain[1].get('version'), "2.3.3") @patch("builtins.open", new_callable=mock_open, read_data=product_yml) def test_app_store_explain_product(self, with_open): # 正向解析产品 upload_obj = UploadPackageHistory.objects.get( operation_uuid='test-uuid') public_action = PublicAction(upload_obj.package_md5) explain = ExplainYml(public_action, '/data/test').explain_yml() upload_obj.refresh_from_db() self.assertEqual(explain[0], True) self.assertEqual(explain[1].get('version'), "5.2.0") class SimulationRedis(object): def exists(self, key): return 0 def lpush(self, key, *args): return True def expire(self, key, expire): return True def lindex(self, key, index): return 'test_redis' publish_info = """\ {"kind": "product", "name": "jenkins", "version": "1.0.0", "description": "描述",\ "dependencies": [{"name": "jdk1.88", "version": "1.88"},\ {"name": "tomcat1.88", "version": "1.88"},\ {"name": "home1.99", "version": "1.99"}],\ "extend_fields": {},\ "service": [{"name": "jenkinss01"},\ {"name": "jenkinsss01"},\ {"name": "jenkinssss01"},\ {"name": "qqqqqqqqqq01"},\ {"name": "jenkins"}],\ "labels": ["mysql_db"],\ "product_service":\ [{"kind": "service", "name": "jenkins", "version": "123",\ "dependencies": [{"name": "jdk", "version": "8u211"}],\ "base_env": "False",\ "extend_fields": {"auto_launch": "true","affinity":"","level":"1",\ "resources": {"cpu": "1000m", "memory": "2000m"}},\ "ports": [{"name": "服务端口", "protocol": "TCP", "key": "service_port", "port": "8080"}],\ "monitor": {"process_name": "jenkins"},\ "control": {"start": "./bin/start.sh", "stop": "./bin/stop.sh",\ "restart": "./bin/restart.sh", "reload": "./bin/reload.sh", "install": "./scripts/install.sh",\ "init": "./scripts/init.sh"},\ "install": [{"name": "安装目录", "key": "base_dir", "default": "{data_path}/jeknins"}],\ "package_name": "jenkins-1.0.0-service.tar.gz"}],\ "image": null, "package_name": "jenkins-1.0.0-test-md5.tar.gz",\ "tmp_dir": ["/data/omp/package_hub/front_end_verified/jenkins-test40yp18cfwbz/jenkins", "00011123"]}\ """ class PackagePublishTest(AutoLoginTest): # 发布逻辑 def setUp(self): super(PackagePublishTest, self).setUp() self.publish_url = reverse( "publish-list") upload_obj = UploadPackageHistory( operation_uuid='test-uuid', operation_user='admin', package_name='jenkins-1.0.0-test-md5.tar.gz', package_md5='test-md5', package_path="verified", package_status=0 ) upload_obj.save() UploadPackageHistory.objects.create( operation_uuid='test-uuid', operation_user='admin', package_name='jenkins-1.0.0-service.tar.gz', package_md5='test-md5-service', package_path="verified/jenkins-1.0.0", package_status=0, package_parent=upload_obj ) @patch("builtins.open", new_callable=mock_open, read_data=publish_info) @mock.patch( "app_store.tasks.exec_clear", return_value="" ) @mock.patch.object( public_utils, "local_cmd", return_value=("", "", 0) ) @mock.patch( "os.path.isfile", return_value=False ) def test_app_store_publish(self, isfile, local_cmd, exe_clear, with_open): # 正向发布产品,包含服务,前端 upload_obj = UploadPackageHistory.objects.get(operation_uuid='test-uuid', package_parent__isnull=True ) publish_entry(upload_obj.operation_uuid) upload_obj.refresh_from_db() app_count = ApplicationHub.objects.filter( app_name="jenkins", app_version="123" ).count() pro_count = ProductHub.objects.filter( pro_name="jenkins", pro_version="1.0.0" ).count() label_count = Labels.objects.filter( label_name="mysql_db" ).count() self.assertEqual(app_count, 1) self.assertEqual(pro_count, 1) self.assertEqual(label_count, 1) @mock.patch( "redis.Redis.delete", return_value="" ) @mock.patch( "app_store.tasks.publish_entry", return_value="" ) def test_app_store_publish_back_true(self, publish, redis): # 正向后端发布等待 res = publish_bak_end('test-uuid', 1) self.assertEqual(res, None) @mock.patch( "app_store.tasks.exec_clear", return_value="" ) @mock.patch( "redis.Redis.delete", return_value="" ) def test_app_store_publish_back(self, redis, exe_clear): # 反向后端发布等待 upload_obj = UploadPackageHistory.objects.get(operation_uuid='test-uuid', package_parent__isnull=True ) upload_obj.package_status = 1 upload_obj.save() res = publish_bak_end('test-uuid', 1) self.assertEqual(res, None) @mock.patch( "app_store.tasks.publish_entry.delay", return_value="" ) def test_app_store_publish_api(self, delay): # 正向post发布接口 resp = self.post(self.publish_url, { "uuid": 'test-uuid', }).json() self.assertDictEqual(resp.get('data'), { "status": "发布任务下发成功" }) # 正向get请求,过滤状态3,4,5 upload_obj1 = UploadPackageHistory.objects.get(operation_uuid='test-uuid', package_parent__isnull=True ) upload_obj1.package_status = 0 upload_obj1.save() resp = self.get(self.publish_url, data={ "operation_uuid": 'test-uuid', }).json() self.assertDictEqual( resp, { 'code': 0, 'message': 'success', 'data': [] } ) @mock.patch.object( public_utils, "local_cmd", return_value=("", "", 0) ) def test_app_store_publish_clear(self, local_cmd): # 正向删除 upload_obj1 = UploadPackageHistory.objects.get(operation_uuid='test-uuid', package_parent__isnull=True ) upload_obj1.package_status = 3 upload_obj1.save() resp = exec_clear('/data/omp/package_hub/front_end_verified') self.assertEqual(resp, None) @mock.patch( "redis.Redis", return_value=SimulationRedis()) @mock.patch( "os.listdir", return_value=["jdk-1.8.1.tar.gz"]) @mock.patch( "os.path.isfile", return_value=True) @mock.patch( "app_store.tasks.front_end_verified.delay", return_value="") @mock.patch( "app_store.tasks.publish_bak_end.delay", return_value="") def test_app_store_scan(self, bak, front, isfile, listdir, redis): uuid, exec_name = back_end_verified_init('admin') count = UploadPackageHistory.objects.filter( operation_uuid=uuid).count() self.assertEqual(count, 1) self.assertEqual(exec_name[0], "jdk-1.8.1.tar.gz") ================================================ FILE: omp_server/tests/test_app_store/test_execute_package_scan.py ================================================ # -*- coding: utf-8 -*- # Project: test_execute_package_scan # Author: jon.liu@yunzhihui.com # Create time: 2021-10-19 21:04 # IDE: PyCharm # Version: 1.0 # Introduction: """ 服务端本地扫描测试方法 """ import uuid from unittest import mock from rest_framework.reverse import reverse from tests.base import AutoLoginTest from db_models.models import UploadPackageHistory class ExecutePackageScanTest(AutoLoginTest): def setUp(self): super(ExecutePackageScanTest, self).setUp() self.executeLocalPackageScan_url = reverse( "executeLocalPackageScan-list") @mock.patch( "app_store.tmp_exec_back_task.back_end_verified_init", return_value=("uuid", [])) def test_request_success(self, mock_obj): resp = self.post( url=self.executeLocalPackageScan_url, data=None).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": { "uuid": "uuid", "package_names": [] } }) class LocalPackageScanResultTest(AutoLoginTest): def setUp(self): super(LocalPackageScanResultTest, self).setUp() self.localPackageScanResult_url = reverse( "localPackageScanResult-list") self.operation_uuid = str(uuid.uuid4()) self.operation_user = "admin" self.package_name_pre = "test_" self.package_path = "/tmp" self.package_names_lst = list() upload_package_history = list() for i in range(10): _obj = UploadPackageHistory( operation_uuid=self.operation_uuid, operation_user=self.operation_user, package_name=self.package_name_pre + f"{str(i)}.tar.gz", package_md5=str(uuid.uuid4()) ) self.package_names_lst.append( self.package_name_pre + f"{str(i)}.tar.gz") upload_package_history.append(_obj) UploadPackageHistory.objects.bulk_create(upload_package_history) def test_get_failed_1(self): resp = self.get(url=self.localPackageScanResult_url) self.assertEqual(resp.json().get("code"), 1) self.assertEqual( resp.json().get("message"), "请求参数中必须包含 [uuid] 字段") def test_get_failed_2(self): resp = self.get( url=self.localPackageScanResult_url, data={ "uuid": self.operation_uuid } ) self.assertEqual( resp.json().get("message"), "请求参数中必须包含 [package_names] 字段") def get_success_resp(self): """ 获取响应值 :return: """ return self.get( url=self.localPackageScanResult_url, data={ "uuid": self.operation_uuid, "package_names": ",".join(self.package_names_lst) } ) def test_get_success(self): self.assertEqual(self.get_success_resp().json().get("code"), 0) def test_checking_status(self): res_dic = self.get_success_resp().json() status = res_dic.get("data", {}).get("stage_status") self.assertEqual(status, "checking") def test_check_all_failed_status(self): UploadPackageHistory.objects.filter( operation_uuid=self.operation_uuid).update( package_status=1) res_dic = self.get_success_resp().json() status = res_dic.get("data", {}).get("stage_status") self.assertEqual(status, "check_all_failed") def test_published_status(self): UploadPackageHistory.objects.filter( operation_uuid=self.operation_uuid).update( package_status=3) res_dic = self.get_success_resp().json() status = res_dic.get("data", {}).get("stage_status") self.assertEqual(status, "published") def test_publishing_status(self): UploadPackageHistory.objects.filter( operation_uuid=self.operation_uuid).update( package_status=5) res_dic = self.get_success_resp().json() status = res_dic.get("data", {}).get("stage_status") self.assertEqual(status, "publishing") ================================================ FILE: omp_server/tests/test_app_store/test_get_application_template.py ================================================ # -*- coding: utf-8 -*- # Project: test_get_application_template # Author: jon.liu@yunzhihui.com # Create time: 2021-10-26 11:33 # IDE: PyCharm # Version: 1.0 # Introduction: from rest_framework.reverse import reverse from django.http.response import FileResponse from tests.base import AutoLoginTest class ApplicationTemplateTest(AutoLoginTest): """ 主机批量校验测试类 """ def setUp(self): super(ApplicationTemplateTest, self).setUp() self.get_template_url = reverse("applicationTemplate-list") def test_get_application_template(self): """ 获取应用商店导入模板 """ # 获取应用商店导入模板 -> 返回文件 resp = self.get(self.get_template_url) self.assertEqual(resp.status_code, 200) self.assertTrue(isinstance(resp, FileResponse)) self.assertTrue(resp.streaming) self.assertIsNotNone(resp.streaming_content) ================================================ FILE: omp_server/tests/test_app_store/test_install_executor.py ================================================ import random from unittest import mock from django.test import TestCase from app_store.install_executor import InstallServiceExecutor from db_models.models import MainInstallHistory from utils.plugin.salt_client import SaltClient from tests.mixin import InstallHistoryResourceMixin class TestInstallExecutor(TestCase, InstallHistoryResourceMixin): """ 安装执行器测试类 """ def test_action(self): """ 测试动作 """ main_obj, detail_obj_ls = self.get_install_history() executor = InstallServiceExecutor(main_obj.id, "admin") # 发送执行正常 with mock.patch.object(SaltClient, "cp_file") as mock_cp_file: mock_cp_file.return_value = True, "success" # 发送服务包 detail_obj = random.choice(detail_obj_ls) is_success, _ = executor.send(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.send_flag, 2) # 发送执行异常 with mock.patch.object(SaltClient, "cp_file") as mock_cp_file: mock_cp_file.return_value = False, "failed" # 发送服务包 detail_obj = random.choice(detail_obj_ls) is_success, _ = executor.send(detail_obj) self.assertFalse(is_success) self.assertEqual(detail_obj.send_flag, 3) # 命令执行正常 with mock.patch.object(SaltClient, "cmd") as mock_cmd: mock_cmd.return_value = True, "success" detail_obj = random.choice(detail_obj_ls) # 解压服务包 is_success, _ = executor.unzip(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.unzip_flag, 2) # 安装 is_success, _ = executor.install(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.install_flag, 2) # 初始化 is_success, _ = executor.init(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.init_flag, 2) # 启动 is_success, _ = executor.start(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.start_flag, 2) # 命令执行异常 with mock.patch.object(SaltClient, "cmd") as mock_cmd: mock_cmd.return_value = False, "failed" detail_obj = random.choice(detail_obj_ls) # 解压服务包 is_success, _ = executor.unzip(detail_obj) self.assertFalse(is_success) self.assertEqual(detail_obj.unzip_flag, 3) # 安装 is_success, _ = executor.install(detail_obj) self.assertFalse(is_success) self.assertEqual(detail_obj.install_flag, 3) # 初始化 is_success, _ = executor.init(detail_obj) self.assertFalse(is_success) self.assertEqual(detail_obj.init_flag, 3) # 启动 is_success, _ = executor.start(detail_obj) self.assertFalse(is_success) self.assertEqual(detail_obj.start_flag, 3) # 服务无 init、start 脚本 detail_obj = random.choice(detail_obj_ls) detail_obj.service.service_controllers.pop("init") detail_obj.service.service_controllers.pop("start") detail_obj.service.save() # 初始化 -> 成功 (跳过) is_success, _ = executor.init(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.init_flag, 2) # 启动 -> 成功 (跳过) is_success, _ = executor.start(detail_obj) self.assertTrue(is_success) self.assertEqual(detail_obj.start_flag, 2) @mock.patch.object(InstallServiceExecutor, "start", return_value=(True, "")) @mock.patch.object(InstallServiceExecutor, "init", return_value=(True, "")) @mock.patch.object(InstallServiceExecutor, "install", return_value=(True, "")) @mock.patch.object(InstallServiceExecutor, "unzip", return_value=(True, "")) @mock.patch.object(InstallServiceExecutor, "send", return_value=(True, "")) def test_main(self, mock_send, mock_unzip, mock_install, mock_init, mock_start): """ 测试主流程函数 """ main_obj, detail_obj_ls = self.get_install_history() action_ls = (mock_send, mock_unzip, mock_install, mock_init, mock_start) executor = InstallServiceExecutor(main_obj.id, "admin") # 所有动作执行成功 -> 主流程执行成功 executor.main() main_obj.refresh_from_db() self.assertEqual( main_obj.install_status, MainInstallHistory.INSTALL_STATUS_SUCCESS) # 任意动作执行失败 -> 主流程执行失败 action = random.choice(action_ls) action.return_value = False, "failed" executor.main() main_obj.refresh_from_db() self.assertEqual( main_obj.install_status, MainInstallHistory.INSTALL_STATUS_FAILED) ================================================ FILE: omp_server/tests/test_app_store/test_new_install.py ================================================ # -*- coding: utf-8 -*- # Project: test_new_install # Author: jon.liu@yunzhihui.com # Create time: 2021-11-25 16:37 # IDE: PyCharm # Version: 1.0 # Introduction: # import json # # from rest_framework.reverse import reverse # from rest_framework.test import APIClient # # from db_models.models import ( # UserProfile # ) # # from tests.base import BaseTest # from tests.test_app_store.install_data_source import ( # create_product, create_host # ) # # # class BatchInstallEntranceTest(BaseTest): # def setUp(self): # create_host() # create_product() # user = UserProfile.objects.create(username="admin") # self.client = APIClient() # self.client.force_authenticate(user) # self.batchInstallEntrance_url = reverse("batchInstallEntrance-list") # # def test_success_1(self, *args, **kwargs): # res = self.client.get( # path=self.batchInstallEntrance_url # ) # data = res.json().get("data") # self.assertTrue(len(data) != 0) # self.client.get( # path=self.batchInstallEntrance_url, # data={"product_name": "test"} # ) # # # class CreateInstallInfoTest(BaseTest): # def setUp(self): # create_host() # create_product() # user = UserProfile.objects.create(username="admin") # self.client = APIClient() # self.client.force_authenticate(user) # self.createInstallInfo_url = reverse("createInstallInfo-list") # # def test_success_1(self, *args, **kwargs): # res = self.client.get( # path=reverse("batchInstallEntrance-list") # ) # data = res.json().get("data") # unique_key = data["unique_key"] # res = self.client.post( # path=self.createInstallInfo_url, # data=json.dumps({ # "high_availability": False, # "install_product": [ # { # "name": "test", # "version": "1.0.0" # } # ], # "unique_key": unique_key # }), # content_type="application/json" # ) # data = res.json().get("data") # is_continue = data.get("is_continue") # self.assertFalse(is_continue) ================================================ FILE: omp_server/tests/test_app_store/test_upload_package.py ================================================ # -*- coding:utf-8 -*- # Project: test_upload_package # Author:Times.niu@yunzhihui.com # Create time: 2021/10/13 5:13 下午 import os import time from django.conf import settings from rest_framework.reverse import reverse from tests.base import AutoLoginTest from tests.mixin import UploadPackageHistoryMixin from db_models.models import UploadPackageHistory class UploadPackageTest(AutoLoginTest): """ 创建上传文件测试类 """ def setUp(self): super(UploadPackageTest, self).setUp() self.upload_url = reverse("upload-list") def create_fake_file(self, file_end): """ 根据传入的文件后缀(file_end)创建 """ fake_data = "hello world" file_name = "test" + file_end file_path = os.path.join(settings.PROJECT_DIR, "package_hub", file_name) with open(file_path, "w+") as f: f.write(fake_data) return file_path def test_error_field(self): # 不提供uuid file_path = self.create_fake_file(".tar.gz") with open(file_path, "rb") as f: resp = self.client.post( self.upload_url, data={"operation_user": "admin", "file": f, "md5": "dfasdfafadfadfagagate"} ).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[uuid]字段", "data": None }) # 不提供operation_user with open(file_path, "rb") as f: resp = self.client.post( self.upload_url, data={"uuid": "63ece2802559e7a37d01daa686d10c4b", "file": f, "md5": "dfasdfafadfadfagagate"} ).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[operation_user]字段", "data": None }) # 不提供md5 with open(file_path, "rb") as f: resp = self.client.post( self.upload_url, data={"operation_user": "admin", "uuid": "63ece2802559e7a37d01daa686d10c4b", "file": f} ).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[md5]字段", "data": None }) # 不提供file resp = self.post( self.upload_url, data={"uuid": "63ece2802559e7a37d01daa686d10c4b", "operation_user": "admin", "md5": "dfasdfafadfadfagagate"} ).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[file]字段", "data": None }) # 提供非tar tar.gz文件 file_path_err = self.create_fake_file(".dmg") with open(file_path_err, "rb") as f: resp = self.client.post( self.upload_url, data={"uuid": "63ece2802559e7a37d01daa686d10c4b", "operation_user": "admin", "file": f, "md5": "dfasdfafadfadfagagate"} ).json() self.assertDictEqual(resp, { "code": 1, "message": "上传文件名仅支持.tar或.tar.gz", "data": None }) def test_correct_field(self): # file_path = self.create_fake_file(".tar.gz") # with open(file_path, "rb") as f: # resp = self.client.post( # self.upload_url, # data={"uuid": "63ece2802559e7a37d01daa686d10c4b", "operation_user": "admin", "file": f} # ).json() # self.assertDictEqual(resp, { # "code": 0, # "message": "success", # "data": { # "uuid": "63ece280-2559-e7a3-7d01-daa686d10c4b", # "operation_user": "admin", # "file": None # } # }) pass def tearDown(self): super(UploadPackageTest, self).tearDown() try: os.remove(os.path.join( settings.PROJECT_DIR, "package_hub", "test.tar.gz")) os.remove(os.path.join( settings.PROJECT_DIR, "package_hub", "test.dmg")) except Exception: pass class RemovePackageTest(AutoLoginTest, UploadPackageHistoryMixin): """ 移除安装包测试类 """ def setUp(self): super(RemovePackageTest, self).setUp() self.remove_package_url = reverse("remove-list") def test_error_field(self): """ 测试错误字段 """ history_objs = self.get_upload_package_history(number=1) operation_uuid = history_objs[0].operation_uuid package_name_ls = list( history_objs.values_list("package_name", flat=True)) # 不提供 uuid -> 移除失败 resp = self.post(self.remove_package_url, { "package_names": package_name_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[uuid]字段", "data": None }) # 提供无效 uuid -> 移除失败 resp = self.post(self.remove_package_url, { "uuid": str(int(round(time.time() * 1000))), "package_names": package_name_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": "该 uuid 未找到有效的操作记录", "data": None }) # 不提供 package_names -> 移除失败 resp = self.post(self.remove_package_url, { "uuid": operation_uuid, }).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[package_names]字段", "data": None }) # 提供无效 package_names -> 移除失败 resp = self.post(self.remove_package_url, { "uuid": operation_uuid, "package_names": ["111", "222", "333"] }).json() self.assertDictEqual(resp, { "code": 1, "message": "该 uuid 未找到有效的操作记录", "data": None }) self.destroy_upload_package_history() def test_correct_field(self): """ 测试正确字段 """ history_objs = self.get_upload_package_history(number=1) operation_uuid = history_objs[0].operation_uuid package_name_ls = list( history_objs.values_list("package_name", flat=True)) # 正确参数 -> 移除成功 resp = self.post(self.remove_package_url, { "uuid": operation_uuid, "package_names": package_name_ls }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") # 安装包历史记录标记软删除 queryset = UploadPackageHistory.objects.filter( operation_uuid=operation_uuid, package_name__in=package_name_ls) for history in queryset: self.assertTrue(history.is_deleted) self.destroy_upload_package_history() ================================================ FILE: omp_server/tests/test_hosts/__init__.py ================================================ ================================================ FILE: omp_server/tests/test_hosts/test_celery_tasks.py ================================================ # -*- coding: utf-8 -*- # Project: test_celery_tasks # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 20:11 # IDE: PyCharm # Version: 1.0 # Introduction: """ 主机相关异步任务单元测试 """ from unittest import mock from tests.base import BaseTest from utils.plugin.agent_util import Agent from utils.plugin.ssh import SSH from utils.plugin.monitor_agent import MonitorAgentManager from hosts import tasks from hosts.tasks import deploy_agent from hosts.tasks import host_agent_restart from hosts.tasks import real_deploy_agent from hosts.tasks import real_host_agent_restart from hosts.tasks import deploy_monitor_agent from db_models.models import Host class HostCeleryTaskTest(BaseTest): """ 主机Agent的测试类 """ def setUp(self): super(HostCeleryTaskTest, self).setUp() self.correct_host_data = { "instance_name": "mysql_instance_1", "ip": "127.0.0.10", "port": 36000, "username": "root", "password": "uea_xeU_d_6YHCCY7Q-e2xZolSw2z2C3KGhLY6iMdnI", "data_folder": "/data", "operate_system": "CentOS", } self.host = Host(**self.correct_host_data) self.host.save() @mock.patch.object(Agent, "agent_deploy", return_value=(True, "success")) @mock.patch.object(tasks, "deploy_monitor_agent", return_value=None) def test_deploy_agent_success(self, *args, **kwargs): """ 测试部署Agent成功 :return: """ self.assertEqual(deploy_agent(self.host.id), None) @mock.patch.object( Agent, "agent_deploy", return_value=(False, "error_message")) @mock.patch.object(tasks, "deploy_monitor_agent", return_value=None) def test_deploy_agent_failed(self, *args, **kwargs): """ 测试部署Agent失败 :return: """ self.assertEqual(deploy_agent(self.host.id), None) @mock.patch.object( Agent, "agent_deploy", return_value=(False, "error_message")) @mock.patch.object(tasks, "deploy_monitor_agent", return_value=None) def test_deploy_agent_failed_with_wrong_id(self, *args, **kwargs): """ 测试部署Agent失败,主机id错误 :return: """ self.assertEqual(deploy_agent(1000), None) @mock.patch.object(Agent, "agent_deploy", return_value=(True, "success")) @mock.patch.object(tasks, "deploy_monitor_agent", return_value=None) def test_real_deploy_agent_success(self, *args, **kwargs): """ 测试部署Agent成功 :return: """ self.assertEqual(real_deploy_agent(self.host), None) @mock.patch.object( Agent, "agent_deploy", return_value=(False, "error_message")) @mock.patch.object(tasks, "deploy_monitor_agent", return_value=None) def test_real_deploy_agent_failed(self, *args, **kwargs): """ 测试部署Agent失败 :return: """ self.assertEqual(real_deploy_agent(self.host), None) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_restart_agent_success(self, agent_deploy): """ 测试重启主机Agent成功 :return: """ self.assertEqual(host_agent_restart(self.host.id), None) @mock.patch.object(SSH, "cmd", return_value=(False, "error_message")) def test_restart_agent_failed(self, agent_deploy): """ 测试重启主机Agent失败 :return: """ self.assertEqual(host_agent_restart(self.host.id), None) @mock.patch.object(SSH, "cmd", return_value=(False, "error_message")) def test_restart_agent_failed_with_wrong_id(self, agent_deploy): """ 测试重启主机Agent失败,主机id错误 :return: """ self.assertEqual(host_agent_restart(1000), None) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_real_restart_agent_success(self, agent_deploy): """ 测试重启主机Agent成功 :return: """ self.assertEqual(real_host_agent_restart(self.host), None) @mock.patch.object(SSH, "cmd", return_value=(False, "error_message")) def test_real_restart_agent_failed(self, agent_deploy): """ 测试重启主机Agent失败 :return: """ self.assertEqual(real_host_agent_restart(self.host), None) @mock.patch.object( MonitorAgentManager, "install", return_value=(True, "error_message")) def test_deploy_monitor_agent_success(self, *args, **kwargs): """ 测试部署监控Agent函数 成功情况 :param args: :param kwargs: :return: """ self.assertEqual(deploy_monitor_agent(self.host, True), None) @mock.patch.object( MonitorAgentManager, "install", return_value=(True, "error_message")) def test_deploy_monitor_agent_false(self, *args, **kwargs): """ 测试部署监控Agent函数 成功情况 :param args: :param kwargs: :return: """ self.assertEqual(deploy_monitor_agent(self.host, False), None) @mock.patch.object( MonitorAgentManager, "install", return_value=(False, "error_message")) def test_deploy_monitor_agent_failed(self, *args, **kwargs): """ 测试部署监控Agent函数 成功情况 :param args: :param kwargs: :return: """ self.assertEqual(deploy_monitor_agent(self.host, True), None) ================================================ FILE: omp_server/tests/test_hosts/test_hosts.py ================================================ import random import string from datetime import datetime from unittest import mock from django.http.response import FileResponse from rest_framework.reverse import reverse from tests.base import AutoLoginTest from tests.mixin import ( HostsResourceMixin, HostBatchRequestMixin, GrafanaMainPageResourceMixin ) from hosts.views import HostListView from hosts.tasks import ( host_agent_restart, insert_host_celery_task ) from hosts.hosts_serializers import HostSerializer from db_models.models import ( Host, HostOperateLog ) from utils.plugin.ssh import SSH from utils.plugin.crypto import AESCryptor from promemonitor.prometheus import Prometheus from promemonitor.alertmanager import Alertmanager class CreateHostTest(AutoLoginTest, HostsResourceMixin): """ 创建主机测试类 """ def setUp(self): super(CreateHostTest, self).setUp() self.create_host_url = reverse("hosts-list") # 正确主机数据 self.correct_host_data = { "instance_name": "mysql_instance_1", "ip": "127.0.0.10", "port": 36000, "username": "root", "password": "root_password", "data_folder": "/data", "operate_system": "CentOS", } def test_error_field_instance_name(self): """ 测试错误字段校验,instance_name """ # 不提供 instance_name -> 创建失败 data = self.correct_host_data.copy() data.pop("instance_name") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[instance_name]字段", "data": None }) # instance_name 超过长度 -> 创建失败 data = self.correct_host_data.copy() data.update( {"instance_name": "north_host_instance_name_mysql_node_one"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名长度需小于16", "data": None }) # instance_name 含中文 -> 创建失败 data = self.correct_host_data.copy() data.update({"instance_name": "mysql实例节点1"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名不可含有中文", "data": None }) # instance_name 含有表情 -> 创建失败 data = self.correct_host_data.copy() data.update({"instance_name": "mysql😃1"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名不可含有表情", "data": None }) # instance_name 不以字母、数字、- 开头 -> 创建失败 data = self.correct_host_data.copy() data.update({"instance_name": "$mysql-01"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名格式不合法", "data": None }) # instance_name 已存在 -> 创建失败 host_obj = self.get_hosts(1)[0] data = self.correct_host_data.copy() data.update({"instance_name": host_obj.instance_name}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名已经存在", "data": None }) self.destroy_hosts() def test_error_field_ip(self): """ 测试错误字段校验,ip """ # 不提供 ip -> 创建失败 data = self.correct_host_data.copy() data.pop("ip") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[ip]字段", "data": None }) # ip 格式不规范 -> 创建失败 data = self.correct_host_data.copy() data.update({"ip": "120.100.80"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "IP格式不合法", "data": None }) # ip 已存在 -> 创建失败 host_obj = self.get_hosts(1)[0] data = self.correct_host_data.copy() data.update({"ip": host_obj.ip}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "IP已经存在", "data": None }) self.destroy_hosts() def test_error_field_port(self): """ 测试错误字段校验,port """ # 不提供 port -> 创建失败 data = self.correct_host_data.copy() data.pop("port") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[port]字段", "data": None }) # port 超过范围 -> 创建失败 data = self.correct_host_data.copy() data.update({"port": 66666}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "端口超出指定范围", "data": None }) def test_error_field_username(self): """ 测试错误字段校验,username """ # 不提供 username -> 创建失败 data = self.correct_host_data.copy() data.pop("username") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[username]字段", "data": None }) # username 超过指定长度 -> 创建失败 data = self.correct_host_data.copy() data.update({"username": "this_is_a_too_lang_username"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "用户名长度需小于16", "data": None }) # username 不以数字、字母、_ 开头 -> 创建失败 data = self.correct_host_data.copy() data.update({"username": "$my_username"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "用户名格式不合法", "data": None }) def test_error_field_password(self): """ 测试错误字段校验,password """ # 不提供 password -> 创建失败 data = self.correct_host_data.copy() data.pop("password") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[password]字段", "data": None }) # password 小于指定长度 -> 创建失败 data = self.correct_host_data.copy() data.update({"password": "pass11"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "密码长度需大于8", "data": None }) # password 超过指定长度 -> 创建失败 data = self.correct_host_data.copy() to_long_password = ''.join(random.choice( string.ascii_letters) for _ in range(70)) data.update({"password": to_long_password}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "密码长度需小于64", "data": None }) # password 含有中文 -> 创建失败 data = self.correct_host_data.copy() data.update({"password": "mysql节点密码"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "密码不可含有中文", "data": None }) # password 含有表情 -> 创建失败 data = self.correct_host_data.copy() data.update({"password": "password😊mysql"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "密码不可含有表情", "data": None }) def test_error_field_data_folder(self): """ 测试错误字段校验,data_folder """ # 不提供 data_folder -> 创建失败 data = self.correct_host_data.copy() data.pop("data_folder") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[data_folder]字段", "data": None }) # data_folder 不以 '/' 开头 -> 创建失败 data = self.correct_host_data.copy() data.update({"data_folder": "data"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "数据分区格式不合法", "data": None }) # data_folder 目录以 '-' 开头 -> 创建失败 data = self.correct_host_data.copy() data.update({"data_folder": "/data/-myDir"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "数据分区目录不能以'-'开头", "data": None }) def test_error_field_operate_system(self): """ 测试错误字段校验,operate_system """ # 不提供 operate_system -> 创建失败 data = self.correct_host_data.copy() data.pop("operate_system") resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "必须包含[operate_system]字段", "data": None }) # 不支持的 operate_system -> 创建失败 data = self.correct_host_data.copy() data.update({"operate_system": "SUSE"}) resp = self.post(self.create_host_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "操作系统支持CentOS/RedHat", "data": None }) @mock.patch.object(SSH, "check", return_value=(False, "error message")) def test_wrong_ssh(self, ssh_mock): """ 测试创建主机,SSH 校验未通过""" # 正确字段,ssh 校验未通过 -> 创建失败 resp = self.post(self.create_host_url, self.correct_host_data).json() self.assertDictEqual(resp, { "code": 1, "message": "SSH登录失败", "data": None }) # @mock.patch.object(SSH, "check", return_value=(True, "")) # @mock.patch.object(SSH, "is_sudo", return_value=(False, "is sudo")) # def test_wrong_username(self, si_sudo, ssh_mock): # """ 测试创建主机,SSH 用户 sudo 权限未通过 """ # # # 正确字段,ssh 校验未通过 -> 创建失败 # resp = self.post(self.create_host_url, self.correct_host_data).json() # self.assertDictEqual(resp, { # "code": 1, # "message": "用户权限错误,请使用root或具备sudo免密用户", # "data": None # }) @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_correct_field(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试正确字段 """ # 正确字段 -> 创建成功 resp = self.post(self.create_host_url, self.correct_host_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") host_info = resp.get("data") self.assertIsNotNone(host_info) for k, v in self.correct_host_data.items(): # 密码字段加密处理,不相等 if k == "password": self.assertNotEqual(host_info.get(k), v) continue # 各字段值相等 self.assertEqual(host_info.get(k), v) # 服务数和告警为 0 self.assertEqual(host_info.get("service_num"), 0) self.assertEqual(host_info.get("alert_num"), 0) # 主机 Agent 和监控 Agent 默认为部署中 self.assertEqual( host_info.get("host_agent"), Host.AGENT_DEPLOY_ING) self.assertEqual( host_info.get("monitor_agent"), Host.AGENT_DEPLOY_ING) # 维护模式默认不开启 self.assertEqual(host_info.get("is_maintenance"), False) # 数据库 -> 主机存在 host_obj = Host.objects.filter(id=host_info.get("id")).first() self.assertIsNotNone(host_obj) # 密码字段 -> 加密处理 self.assertNotEqual( host_obj.password, self.correct_host_data.get("password") ) aes = AESCryptor() self.assertEqual( aes.decode(host_obj.password), self.correct_host_data.get("password") ) # 软删除字段 -> False self.assertEqual(host_obj.is_deleted, False) # 删除主机 host_obj.delete(soft=False) class ListHostTest(AutoLoginTest, HostsResourceMixin, GrafanaMainPageResourceMixin): """ 主机列表测试类 """ def setUp(self): super(ListHostTest, self).setUp() self.create_host_url = reverse("hosts-list") self.list_host_url = reverse("hosts-list") self.get_grafana_main_pages() self.host_obj_ls = self.get_hosts(50) def tearDown(self): super(ListHostTest, self).tearDown() self.destroy_hosts() @staticmethod def mock_prometheus_info(host_obj_ls): """ 模拟 prometheus 返回数据 """ for host in host_obj_ls: host.update({ "cpu_usage": random.choice( [None, random.randint(0, 100)]), "mem_usage": random.choice( [None, random.randint(0, 100)]), "root_disk_usage": random.choice( [None, random.randint(0, 100)]), "data_disk_status": random.choice( [None, random.randint(0, 100)]), "cpu_status": random.choice( [None, random.choice(Prometheus.STATUS)]), "mem_status": random.choice( [None, random.choice(Prometheus.STATUS)]), "data_disk_usage": random.choice( [None, random.choice(Prometheus.STATUS)]), "root_disk_status": random.choice( [None, random.choice(Prometheus.STATUS)]), }) return host_obj_ls def test_hosts_list_filter(self): """ 测试主机列表过滤 """ # 查询主机列表 -> 展示所有主机 resp = self.get(self.list_host_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get("data")) # 数据总量为所有主机数 self.assertEqual(resp.get("data").get("count"), len(self.host_obj_ls)) # IP 过滤主机 -> 展示 IP 模糊匹配项 ip_field = str(random.randint(1, 50)) resp = self.get(self.list_host_url, { "ip": ip_field }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get("data")) count_number = Host.objects.filter(ip__contains=ip_field).count() self.assertEqual(resp.get("data").get("count"), count_number) def test_hosts_list_order(self): """ 测试主机列表排序 """ # 不传递排序字段 -> 默认按照主机创建时间排序 resp = self.get(self.list_host_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") res_ls = resp.get("data").get("results") sorted_res_ls = res_ls[:] random.shuffle(sorted_res_ls) sorted_res_ls = sorted( sorted_res_ls, key=lambda x: datetime.strptime( x.get("created"), "%Y-%m-%dT%H:%M:%S.%f"), reverse=True) self.assertEqual(res_ls, sorted_res_ls) # 指定字段排序 -> 返回排序后的列表 reverse_flag = random.choice(("", "-")) order_field = random.choice(HostListView.ordering_fields) resp = self.get(self.list_host_url, { "ordering": f"{reverse_flag}{order_field}" }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") res_ls = list(map(lambda x: x.get(order_field), resp.get("data").get("results"))) sorted_res_ls = res_ls[:] random.shuffle(sorted_res_ls) sorted_res_ls = sorted( sorted_res_ls, reverse=True if reverse_flag else False) self.assertEqual(res_ls, sorted_res_ls) # 指定动态排序字段 -> 返回值为None的不参与排序 reverse_flag = random.choice(("", "-")) order_field = random.choice(HostListView.dynamic_fields) host_obj_ls = HostSerializer(Host.objects.all(), many=True).data with mock.patch.object(Prometheus, "get_host_info") as mock_prometheus_info: mock_prometheus_info.return_value = self.mock_prometheus_info( host_obj_ls) resp = self.get(self.list_host_url, { "ordering": f"{reverse_flag}{order_field}" }).json() # 返回值为 None 的数据不参与排序,排在末尾位置 res_ls = list(map(lambda x: x.get(order_field), resp.get("data").get("results"))) none_number = res_ls.count(None) self.assertTrue(not any(res_ls[-none_number:])) res_ls = list(filter(lambda x: x is not None, res_ls)) sorted_res_ls = res_ls[:] random.shuffle(sorted_res_ls) sorted_res_ls = sorted( sorted_res_ls, reverse=True if reverse_flag else False) self.assertEqual(res_ls, sorted_res_ls) class HostDetailTest(AutoLoginTest, HostsResourceMixin): """ 主机详情测试类 """ def setUp(self): super(HostDetailTest, self).setUp() self.host_obj_ls = self.get_hosts() def tearDown(self): super(HostDetailTest, self).tearDown() self.destroy_hosts() def test_host_detail(self): """ 测试主机详情 """ resp = self.get(reverse("hostsDetail-detail", [ random.choice(self.host_obj_ls).id ])).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get("data")) class UpdateHostTest(AutoLoginTest, HostsResourceMixin): """ 更新主机测试类 """ def setUp(self): super(UpdateHostTest, self).setUp() self.host_obj_ls = self.get_hosts() def tearDown(self): super(UpdateHostTest, self).tearDown() self.destroy_hosts() @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) def test_update_host(self, cmd_mock, is_sudo, ssh_mock): """ 测试更新一个主机 """ # 更新不存在主机 -> 更新失败 resp = self.put(reverse("hosts-detail", [9999]), { "instance_name": "mysql_instance_1", "ip": "127.0.0.255", "port": 36000, "username": "root", "password": "root_password", "data_folder": "/data", "operate_system": "CentOS", }).json() self.assertDictEqual(resp, { "code": 1, "message": "未找到", "data": None }) # 更新已存在主机,修改主机 IP -> 更新失败 host_obj = random.choice(self.host_obj_ls) resp = self.put(reverse("hosts-detail", [host_obj.id]), { "instance_name": host_obj.instance_name, "ip": "127.0.0.255", "port": host_obj.port, "username": host_obj.username, "password": AESCryptor().decode(host_obj.password), "data_folder": host_obj.data_folder, "operate_system": host_obj.operate_system, }).json() self.assertDictEqual(resp, { "code": 1, "message": "IP不可修改", "data": None }) # 更新已存在主机,修改实例名为已存在 -> 更新失败 host_obj = random.choice(self.host_obj_ls) host_queryset = Host.objects.exclude( instance_name=host_obj.instance_name) exists_name = random.choice(host_queryset).instance_name resp = self.put(reverse("hosts-detail", [host_obj.id]), { "instance_name": exists_name, "ip": host_obj.ip, "port": host_obj.port, "username": host_obj.username, "password": AESCryptor().decode(host_obj.password), "data_folder": host_obj.data_folder, "operate_system": host_obj.operate_system, }).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名已经存在", "data": None }) # 正确修改数据 -> 修改成功 host_obj = random.choice(self.host_obj_ls) resp = self.put(reverse("hosts-detail", [host_obj.id]), { "instance_name": "new_host_name", "ip": host_obj.ip, "port": host_obj.port, "username": "new_username", "password": "new_password", "data_folder": host_obj.data_folder, "operate_system": host_obj.operate_system, }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") new_host_info = resp.get("data") # 数据已更新 self.assertEqual(new_host_info.get("instance_name"), "new_host_name") # 更新时间变化 self.assertNotEqual( host_obj.modified, Host.objects.filter(id=host_obj.id).first().modified) @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) def test_partial_update_host(self, cmd, is_sudo, ssh_mock): """ 更新一个现有主机的一个或多个字段 """ # 更新不存在主机 -> 更新失败 resp = self.patch(reverse("hosts-detail", [9999]), { "instance_name": "new_host_name", }).json() self.assertDictEqual(resp, { "code": 1, "message": "未找到", "data": None }) # 更新已存在主机,修改主机 IP -> 更新失败 host_obj = random.choice(self.host_obj_ls) resp = self.patch(reverse("hosts-detail", [host_obj.id]), { "ip": "120.100.80.60", }).json() self.assertDictEqual(resp, { "code": 1, "message": "IP不可修改", "data": None }) # 更新已存在主机,修改实例名为已存在 -> 更新失败 host_obj = random.choice(self.host_obj_ls) host_queryset = Host.objects.exclude( instance_name=host_obj.instance_name) exists_name = random.choice(host_queryset).instance_name resp = self.patch(reverse("hosts-detail", [host_obj.id]), { "instance_name": exists_name, }).json() self.assertDictEqual(resp, { "code": 1, "message": "实例名已经存在", "data": None }) # 正确修改数据 -> 修改成功 host_obj = random.choice(self.host_obj_ls) resp = self.patch(reverse("hosts-detail", [host_obj.id]), { "instance_name": "new_host_name", "username": "new_username", "password": "new_password", }).json() # self.assertEqual(resp.get("code"), 0) # self.assertEqual(resp.get("message"), "success") # new_host_obj = resp.get("data") # self.assertIsNotNone(new_host_obj) # # 数据已更新 # self.assertEqual(new_host_obj.get("instance_name"), "new_host_name") # # 更新时间变化 # self.assertNotEqual( # host_obj.modified, # Host.objects.filter(id=host_obj.id).first().modified) class HostFieldCheckTest(AutoLoginTest, HostsResourceMixin): """ 主机字段校验测试类 """ def setUp(self): super(HostFieldCheckTest, self).setUp() self.field_check_url = reverse("fields-list") self.host_obj_ls = self.get_hosts() def tearDown(self): self.destroy_hosts() def test_create_host_check(self): """ 测试创建主机场景 """ # instance_name 重复 -> 验证结果 False resp = self.post(self.field_check_url, { "instance_name": random.choice(self.host_obj_ls).instance_name }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": False }) # instance_name 不重复 -> 验证结果 True resp = self.post(self.field_check_url, { "instance_name": "my_host_name" }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": True }) # ip 重复 -> 验证结果 False resp = self.post(self.field_check_url, { "ip": random.choice(self.host_obj_ls).ip }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": False }) # ip 不重复 -> 验证结果 True resp = self.post(self.field_check_url, { "ip": "123.1.2.3" }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": True }) def test_error_host_check(self): """ 测试更新主机场景 """ host_obj_one = random.choice(self.host_obj_ls) host_queryset = Host.objects.exclude( instance_name=host_obj_one.instance_name) host_obj_two = random.choice(host_queryset) # instance_name 重复 (为主机自身 instance_name) -> 验证结果 True resp = self.post(self.field_check_url, { "id": host_obj_one.id, "instance_name": host_obj_one.instance_name }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": True }) # instance_name 重复 (为其他主机 instance_name) -> 验证结果 False resp = self.post(self.field_check_url, { "id": host_obj_one.id, "instance_name": host_obj_two.instance_name }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": False }) # ip 重复 (为主机自身 ip) -> 验证结果 True resp = self.post(self.field_check_url, { "id": host_obj_one.id, "ip": host_obj_one.ip }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": True }) # ip 重复 (为其他主机 ip) -> 验证结果 False resp = self.post(self.field_check_url, { "id": host_obj_one.id, "ip": host_obj_two.ip }).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": False }) class ListIPTest(AutoLoginTest, HostsResourceMixin): """ IP 列表测试类 """ def setUp(self): super(ListIPTest, self).setUp() self.ip_list_url = reverse("ips-list") self.get_hosts() def tearDown(self): super(ListIPTest, self).tearDown() self.destroy_hosts() def test_ip_list(self): """ 测试 IP 列表 """ # 查询主机列表 -> 返回所有主机列表数据 resp = self.get(self.ip_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( set(resp.get("data")), set(Host.objects.all().values_list("ip", flat=True))) class HostMaintainTest(AutoLoginTest, HostsResourceMixin): """ 主机维护模式测试类 """ def setUp(self): super(HostMaintainTest, self).setUp() self.host_maintain_url = reverse("maintain-list") self.host_obj_ls = self.get_hosts() def tearDown(self): super(HostMaintainTest, self).tearDown() self.destroy_hosts() def test_error_field(self): """ 测试错误字段校验 """ host_obj_id_ls = list(map(lambda x: x.id, self.host_obj_ls)) # host_ids 中含不存在的 ID -> 修改失败 not_exists_id = 9999 random_host_ls = random.sample(host_obj_id_ls, 5) random_host_ls.append(not_exists_id) resp = self.post(self.host_maintain_url, { "is_maintenance": True, "host_ids": random_host_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": f"主机列表中有不存在的ID [{not_exists_id}]", "data": None }) # host_ids 中存在已经处于 type 类型的主机 -> 创建失败 random_host_ls = random.sample(host_obj_id_ls, 5) resp = self.post(self.host_maintain_url, { "is_maintenance": False, "host_ids": random_host_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": "主机列表中存在已 '关闭' 维护模式的主机", "data": None }) @mock.patch.object(Alertmanager, "set_maintain_by_host_list", return_value=[1, 2, 3]) @mock.patch.object(Alertmanager, "revoke_maintain_by_host_list", return_value=[1, 2, 3]) def test_correct_field(self, mock_down, mock_up): """ 正确字段校验 """ random_host_ls = random.sample(list(self.host_obj_ls), 5) random_host_id_ls = list(map(lambda x: x.id, random_host_ls)) # 开启维护模式 -> 开启成功,记录操作 data = { "is_maintenance": True, "host_ids": random_host_id_ls } resp = self.post(self.host_maintain_url, data).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": data }) # host_ids中主机,is_maintenance 状态均为 True is_maintenance_ls = Host.objects.filter( id__in=random_host_id_ls ).values_list("is_maintenance", flat=True) self.assertTrue(all(is_maintenance_ls)) # 主机操作日志含有操作记录 operate_log_ls = HostOperateLog.objects.filter( host__in=random_host_ls, description="开启[维护模式]") self.assertEqual(len(random_host_id_ls), len(operate_log_ls)) self.assertEqual( len(operate_log_ls), len(operate_log_ls.filter(result="success"))) # 关闭维护模式 data = { "is_maintenance": False, "host_ids": random_host_id_ls } resp = self.post(self.host_maintain_url, data).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": data }) # host_ids中主机,is_maintenance 状态均为 False is_maintenance_ls = Host.objects.filter( id__in=random_host_id_ls ).values_list("is_maintenance", flat=True) self.assertTrue(not any(is_maintenance_ls)) # 主机操作日志含有操作记录 operate_log_ls = HostOperateLog.objects.filter( host__in=random_host_ls, description="关闭[维护模式]") self.assertEqual(len(random_host_id_ls), len(operate_log_ls)) self.assertEqual( len(operate_log_ls), len(operate_log_ls.filter(result="success"))) @mock.patch.object(Alertmanager, "set_maintain_by_host_list", return_value=None) @mock.patch.object(Alertmanager, "revoke_maintain_by_host_list", return_value=None) def test_alert_manager_error(self, mock_down, mock_up): """ alert manage 返回值异常 """ random_host_ls = random.sample(list(self.host_obj_ls), 5) random_host_id_ls = list(map(lambda x: x.id, random_host_ls)) # 开始维护模式 -> 开启失败,记录操作 resp = self.post(self.host_maintain_url, { "is_maintenance": True, "host_ids": random_host_id_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": "主机'开启'维护模式失败", "data": None }) # host_ids中主机,is_maintenance 状态均为 False is_maintenance_ls = Host.objects.filter( id__in=random_host_id_ls ).values_list("is_maintenance", flat=True) self.assertTrue(not any(is_maintenance_ls)) # 主机操作日志含有操作记录 operate_log_ls = HostOperateLog.objects.filter( host__in=random_host_ls, description="开启[维护模式]") self.assertEqual(len(random_host_id_ls), len(operate_log_ls)) self.assertEqual( len(operate_log_ls), len(operate_log_ls.filter(result="failed"))) # 关闭维护模式 -> 关闭失败,记录操作 random_host_ls = random.sample(list(self.host_obj_ls), 5) random_host_id_ls = list(map(lambda x: x.id, random_host_ls)) Host.objects.filter( id__in=random_host_id_ls ).update(is_maintenance=True) resp = self.post(self.host_maintain_url, { "is_maintenance": False, "host_ids": random_host_id_ls }).json() self.assertDictEqual(resp, { "code": 1, "message": "主机'关闭'维护模式失败", "data": None }) # host_ids中主机,is_maintenance 状态均为 True is_maintenance_ls = Host.objects.filter( id__in=random_host_id_ls ).values_list("is_maintenance", flat=True) self.assertTrue(all(is_maintenance_ls)) # 主机操作日志含有操作记录 operate_log_ls = HostOperateLog.objects.filter( host__in=random_host_ls, description="关闭[维护模式]") self.assertEqual(len(random_host_id_ls), len(operate_log_ls)) self.assertEqual( len(operate_log_ls), len(operate_log_ls.filter(result="failed"))) class HostAgentRestartTest(AutoLoginTest, HostsResourceMixin): """ 主机 agent 重启测试类 """ def setUp(self): super(HostAgentRestartTest, self).setUp() self.host_restartHostAgent_url = reverse("restartHostAgent-list") self.host_obj_ls = self.get_hosts(2) def tearDown(self): super(HostAgentRestartTest, self).tearDown() self.destroy_hosts() @mock.patch.object(host_agent_restart, "delay", return_value=None) def test_success(self, host_agent_restart_mock): """ 请求成功测试 """ host_obj_id_ls = list(map(lambda x: x.id, self.host_obj_ls)) resp = self.post( self.host_restartHostAgent_url, data={"host_ids": host_obj_id_ls} ).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": { "host_ids": host_obj_id_ls } }) @mock.patch.object(host_agent_restart, "delay", return_value=None) def test_failed(self, host_agent_restart_mock): """ 请求失败测试 """ resp = self.post( self.host_restartHostAgent_url, data={"host_ids": [random.randint(10000, 20000)]} ).json() self.assertEqual(resp.get("code"), 1) class HostBatchValidateTest(AutoLoginTest, HostsResourceMixin, HostBatchRequestMixin): """ 主机批量校验测试类 """ def setUp(self): super(HostBatchValidateTest, self).setUp() self.get_template_url = reverse("batchValidate-list") self.batch_validate_url = reverse("batchValidate-list") @staticmethod def create_repeat_data(host_list, field_name): """ 创建重复数据 """ instance_name = "mysql_{}" ip = "10.0.0.{}" repeat_number = random.randint(2, 5) if field_name == "instance_name" or field_name == "all": instance_name = host_list[repeat_number].get("instance_name") if field_name == "ip" or field_name == "all": ip = host_list[repeat_number].get("ip") for i in range(repeat_number): host_list.append({ "instance_name": instance_name.format(i), "ip": ip.format(i), "port": 36000, "username": "root", "password": "root_password", "data_folder": "/data", "operate_system": random.choice(("CentOS", "RedHat")), # "row": i * 100 }) return host_list, repeat_number def test_get_host_batch_template(self): """ 获取主机批量导入模板 """ # 获取主机批量导入模板 -> 返回文件 resp = self.get(self.get_template_url) self.assertEqual(resp.status_code, 200) self.assertTrue(isinstance(resp, FileResponse)) self.assertTrue(resp.streaming) self.assertIsNotNone(resp.streaming_content) @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_error_format(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试错误格式 """ # 格式错误 -> 添加失败 data = self.get_host_batch_request(10, row=True) data["host_list"].append(12345) resp = self.post(self.batch_validate_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "数据格式错误", "data": None }) @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_batch_validate_error_field(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试批量校验错误字段 """ host_number = 10 # 存在实例名重复 -> 返回值 error 中包含错误信息 data = self.get_host_batch_request(host_number, row=True) data["host_list"], repeat_number = self.create_repeat_data( data.get("host_list"), "instance_name") resp = self.post(self.batch_validate_url, data).json() error_ls = resp.get("data").get("error", []) self.assertEqual(len(error_ls), repeat_number + 1) for error_host_info in error_ls: self.assertEqual( error_host_info.get("validate_error"), "实例名在表格中重复" ) # 存在IP重复 -> 返回值 error 中包含错误信息 data = self.get_host_batch_request(host_number, row=True) data["host_list"], repeat_number = self.create_repeat_data( data.get("host_list"), "ip") resp = self.post(self.batch_validate_url, data).json() error_ls = resp.get("data").get("error", []) self.assertEqual(len(error_ls), repeat_number + 1) for error_host_info in error_ls: self.assertEqual( error_host_info.get("validate_error"), "IP在表格中重复" ) # 存在实例名、IP混合重复 -> 返回值 error 中包含错误信息 data = self.get_host_batch_request(host_number, row=True) data["host_list"], repeat_number = self.create_repeat_data( data.get("host_list"), "all") resp = self.post(self.batch_validate_url, data).json() error_ls = resp.get("data").get("error", []) self.assertEqual(len(error_ls), repeat_number + 1) for error_host_info in error_ls: self.assertEqual( error_host_info.get("validate_error"), "实例名、IP在表格中重复" ) # 测试主机数据信息不合法 -> 返回值 error 中包含错误信息 data = self.get_host_batch_request(host_number, row=True) error_index = random.randint(0, host_number - 1) data.get("host_list")[error_index]["instance_name"] = "中文实例名" resp = self.post(self.batch_validate_url, data).json() error_ls = resp.get("data").get("error", []) self.assertEqual(len(error_ls), 1) self.assertEqual( error_ls[0].get("validate_error"), "实例名不可含有中文; 实例名格式不合法") @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_batch_validate_correct_field(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试批量校验正确字段 """ # 正确字段 -> 返回值全部包含于 correct ,error 中无数据 host_number = 10 data = self.get_host_batch_request(host_number, row=True) resp = self.post(self.batch_validate_url, data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") correct_ls = resp.get("data").get("correct", []) error_ls = resp.get("data").get("error", []) self.assertEqual(len(correct_ls), host_number) self.assertEqual(len(error_ls), 0) # 返回结果按照 row 进行排序 self.assertEqual( correct_ls, list(sorted(correct_ls, key=lambda x: x.get("row"))) ) class HostBatchImportTest(AutoLoginTest, HostsResourceMixin, HostBatchRequestMixin): """ 主机批量导入测试类 """ def setUp(self): super(HostBatchImportTest, self).setUp() self.batch_import_url = reverse("batchImport-list") @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_error_format(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试错误格式 """ # 格式错误 -> 添加失败 data = self.get_host_batch_request(10, row=True) data["host_list"].append(12345) resp = self.post(self.batch_import_url, data).json() self.assertDictEqual(resp, { "code": 1, "message": "数据格式错误", "data": None }) @mock.patch.object(SSH, "check", return_value=(True, "")) @mock.patch.object(SSH, "is_sudo", return_value=(True, "is sudo")) @mock.patch.object(SSH, "cmd", return_value=(True, "")) @mock.patch.object(insert_host_celery_task, "delay", return_value=None) def test_batch_import(self, celery_task_mock, cmd_mock, is_sudo, ssh_mock): """ 测试批量添加主机 """ # 批量添加主机 -> 添加成功 data = self.get_host_batch_request(10, row=True) resp = self.post(self.batch_import_url, data).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": "添加成功" }) ================================================ FILE: omp_server/tests/test_inspection/__init__.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/28 4:58 下午 # Description: ================================================ FILE: omp_server/tests/test_inspection/inspection_mixin.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/29 10:26 上午 # Description: import random from db_models.models import ( InspectionHistory, InspectionCrontab, InspectionReport) class InspectionHistoryMixin: @staticmethod def get_inspection_history(env): h_bulk = list() inspection_type = ["host", "deep", "service"] inspection_status = [1, 2, 3] for i in range(12): h_dict = {'inspection_name': '深度巡检202110271', 'inspection_type': random.choices(inspection_type)[0], 'inspection_status': random.choices(inspection_status)[0], 'execute_type': 'man', 'inspection_operator': 'admin', 'hosts': ["10.0.7.146"], 'env': env} h_bulk.append(InspectionHistory(**h_dict)) InspectionHistory.objects.bulk_create(h_bulk) return h_bulk @staticmethod def create_inspection_crontab(env): _ = {'crontab_detail': {'hour': "09", 'minute': "41", 'month': "*", 'day_of_week': "*", 'day': "*"}, 'env': env, 'is_start_crontab': 0, 'job_name': "深度分析", 'job_type': 0} return InspectionCrontab.objects.create(**_) @staticmethod def update_inspection_crontab(): _ = {'crontab_detail': {'hour': "09", 'minute': "41", 'month': "*", 'day_of_week': "*", 'day': "*"}, 'env': 1, 'is_start_crontab': 0, 'job_name': "深度分析", 'job_type': 0} InspectionCrontab.objects.filter(job_type=_.get('job_type')).update(**_) @staticmethod def get_inspection_crontab(): _ = InspectionCrontab.objects.filter(job_type=0).first() return _ class InspectionReportMixin: @staticmethod def create_inspection_report(env): h_dict = {'inspection_name': '深度巡检202110271', 'inspection_type': 'deep', 'inspection_status': '2', 'execute_type': 'man', 'inspection_operator': 'admin', 'hosts': ["10.0.7.146"], 'env': env} _obj = InspectionHistory.objects.create(**h_dict) InspectionReport(inst_id=_obj).save() return _obj.id @staticmethod def update_inspection_report(inst_id): InspectionReport.objects.filter(inst_id=inst_id).update( risk_data={'host_list': [], 'service': []} ) ================================================ FILE: omp_server/tests/test_inspection/test_crontab.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/28 4:59 下午 # Description: from tests.base import AutoLoginTest from rest_framework.reverse import reverse from tests.test_inspection.inspection_mixin import InspectionHistoryMixin class TestInspectionCrontabList(AutoLoginTest, InspectionHistoryMixin): def setUp(self): super(TestInspectionCrontabList, self).setUp() self.env = self.create_default_env() def tearDown(self): super(TestInspectionCrontabList, self).tearDown() def test_crontab_read(self): _ = self.create_inspection_crontab(self.env) resp = self.get( f"{reverse('crontab-list')}0/", data={'job_type': 0}).json() self.assertEqual(_.id, resp.get('data').get('id')) def test_crontab_create(self): _ = {'crontab_detail': {'hour': "09", 'minute': "41", 'month': "*", 'day_of_week': "1", 'day': "*"}, 'env': self.env, 'is_start_crontab': 0, 'job_name': "深度分析", 'job_type': 0} resp = self.post(reverse("crontab-list"), data=_).json() _obj = self.get_inspection_crontab() self.assertEqual(_obj.id, resp.get('data').get('id')) def test_crontab_update(self): _ = {'env': self.env, 'is_start_crontab': 0, 'job_name': "深度分析", 'job_type': 0, 'crontab_detail': {'hour': "09", 'minute': "41", 'month': "*", 'day_of_week': "1", 'day': "*"} } resp_add = self.post(reverse("crontab-list"), data=_).json() self.assertEqual(resp_add.get("code"), 0) self.assertEqual(resp_add.get("message"), "success") self.assertTrue(resp_add.get("data") is not None) _ = {'crontab_detail': {'hour': "10", 'minute': "41", 'month': "*", 'day_of_week': "1", 'day': "*"}, 'env': 1, 'is_start_crontab': 0, 'job_name': "深度分析2", 'job_type': 0} resp_upd = self.put( f"{reverse('crontab-list')}0/?job_type=0", data=_).json() self.assertEqual(resp_upd.get('data').get('job_name'), "深度分析2") ================================================ FILE: omp_server/tests/test_inspection/test_history.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/28 4:59 下午 # Description: from unittest import mock from tests.base import AutoLoginTest from rest_framework.reverse import reverse from tests.test_inspection.inspection_mixin import InspectionHistoryMixin class TestInspectionHistoryList(AutoLoginTest, InspectionHistoryMixin): def setUp(self): super(TestInspectionHistoryList, self).setUp() self.env = self.create_default_env() def tearDown(self): super(TestInspectionHistoryList, self).tearDown() def test_history_list(self): # 未找到 resp = self.get(reverse("history-list")).json() self.assertDictEqual(resp, { 'code': 0, 'message': 'success', 'data': {'count': 0, 'next': None, 'previous': None, 'results': []} }) # 查询成功 self.get_inspection_history(self.env) resp = self.get(reverse("history-list")).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch("inspection.tasks.get_prometheus_data.delay", return_value=True) def test_history_post(self, tasks): # 深度巡检 data = {'env': 1, 'execute_type': "man", 'hosts': {}, 'inspection_name': "深度巡检", 'inspection_operator': "admin", 'inspection_status': '1', 'inspection_type': "deep", 'services': {}} resp = self.post(reverse("history-list"), data=data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) # 主机巡检 data = {'env': 1, 'execute_type': "man", 'hosts': ["10.0.9.67"], 'inspection_name': "主机巡检", 'inspection_operator': "admin", 'inspection_status': '1', 'inspection_type': "host", 'services': {}} resp = self.post(reverse("history-list"), data=data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) # 组件巡检 data = {'env': 1, 'execute_type': "man", 'hosts': {}, 'inspection_name': "组件巡检", 'inspection_operator': "admin", 'inspection_status': '1', 'inspection_type': "service", 'services': [8]} resp = self.post(reverse("history-list"), data=data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_inspection/test_inspection_email.py ================================================ import json from rest_framework.reverse import reverse from tests.base import AutoLoginTest from tests.test_inspection.inspection_mixin import InspectionHistoryMixin class MockResponse: """ 自定义mock response类 """ status = 200 def __init__(self, data): self.text = json.dumps(data) self.status_code = self.status def json(self): return json.loads(self.text) class InspectionEmail(AutoLoginTest, InspectionHistoryMixin): def setUp(self): super(InspectionEmail, self).setUp() self.inspection_email_config_url = reverse( "inspectionSendEmailSetting-list") self.inspection_send_email_url = reverse("inspectionSendEmail-list") def tearDown(self): super(InspectionEmail, self).tearDown() def test_get_inspection_email_config(self): resp = self.get(self.inspection_email_config_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) def test_update_inspection_email_config(self): post_data = { "env_id": 1, "send_email": True, "to_users": "123@qq.com" } resp = self.post(self.inspection_email_config_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) # def test_inspection_send_email(self): # from db_models.models import Env # Env.objects.get_or_create(name="default") # env = Env.objects.get(id=1) # inspection_history_objs = InspectionHistoryMixin.get_inspection_history( # env=env) # inspection_report_objs = InspectionReportMixin.create_inspection_report( # env=env) # post_data = { # "id": 1, # "module": "deep", # "to_users": "123@qq.com" # } # resp = self.post(self.inspection_send_email_url, data=post_data).json() # print(resp) # self.assertEqual(resp.get("code"), 0) # self.assertEqual(resp.get("message"), "success") # self.assertTrue(resp.get("data") is not None) # for ih_obj in inspection_history_objs: # ih_obj.delete() # Env.objects.filter(name="default").delete() ================================================ FILE: omp_server/tests/test_inspection/test_report.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/28 4:59 下午 # Description: from tests.base import AutoLoginTest from rest_framework.reverse import reverse from tests.test_inspection.inspection_mixin import InspectionReportMixin class TestInspectionReportList(AutoLoginTest, InspectionReportMixin): def setUp(self): super(TestInspectionReportList, self).setUp() self.env = self.create_default_env() def tearDown(self): super(TestInspectionReportList, self).tearDown() def test_crontab_read(self): inst_id = self.create_inspection_report(self.env) self.update_inspection_report(inst_id=inst_id) resp = self.get( f"/api/inspection/report/{inst_id}/", data={'inst_id': inst_id} ).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_promemonitor/__init__.py ================================================ ================================================ FILE: omp_server/tests/test_promemonitor/test_alert.py ================================================ import json from rest_framework.reverse import reverse from unittest import mock from tests.base import AutoLoginTest, BaseTest from db_models.models import Alert class MockResponse: """ 自定义mock response类 """ status_code = 0 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class AlertTest(AutoLoginTest): """ 告警测试类 """ def setUp(self): super(AlertTest, self).setUp() self.list_alert_url = reverse("listAlert-list") self.update_alert_url = reverse("updateAlert-list") # 正确请求数据 self.correct_request_data = {'is_read': 1} Alert.objects.create( is_read=0, alert_type='host', alert_host_ip='10.0.9.61', alert_service_name='', alert_instance_name='doim', alert_service_type='', alert_level='critical', alert_describe='zsh', alert_receiver='test', alert_resolve='', alert_time='2021-06-28 12:00:01', create_time='2021-06-28 12:00:01', monitor_path='-', monitor_log='-', fingerprint='', # env='default' # TODO 此版本默认不赋值 ) def test_get_alerts(self): """ 测试获取告警记录 """ resp = self.get(self.list_alert_url, data={"start_alert_time": "2021-09-11 12:34:56", "end_alert_time": "2021-09-11 12:34:57"}).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") resp = self.get(self.list_alert_url, data={"start_alert_time": "2021-09-11 12:34:56"}).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") error_time_resp = self.get(self.list_alert_url, data={"start_alert_time": "2021-09-11 12:34:56", "end_alert_time": "123456"}).json() self.assertEqual(error_time_resp.get("code"), 0) self.assertEqual(error_time_resp.get("message"), "success") request_post_response = { "code": 0, "message": "success", "data": { "ids": [ 8, 9 ], "is_read": 1 } } @mock.patch.object(BaseTest, 'post', return_value='') def test_update_is_read(self, mock_post): """ 修改已读/未读 """ mock_post.return_value = MockResponse(self.request_post_response) resp = self.post(self.update_alert_url, self.correct_request_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get('data')) def tearDown(self): Alert.objects.filter(alert_host_ip='10.0.9.61').delete() ================================================ FILE: omp_server/tests/test_promemonitor/test_alertmanager.py ================================================ import json from unittest import mock import requests from django.test import TestCase from promemonitor.alertmanager import Alertmanager from db_models.models import MonitorUrl, Maintain class MockResponse: """ 自定义mock response类 """ status = 200 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class TestAlertmanager(TestCase): """ alertmanager功能测试类 """ def setUp(self): MonitorUrl.objects.create( name='alertmanager', monitor_url='127.0.0.1:19013') @staticmethod def return_host_list(): host_list = [ { 'ip': '10.0.3.71', 'data_folder': '/boot', 'cpu_usage': 0, 'mem_usage': 0, 'root_disk_usage': 0, 'data_disk_usage': 0, }, { 'ip': '10.0.3.72', 'data_folder': '/boot', 'cpu_usage': 0, 'mem_usage': 0, 'root_disk_usage': 0, 'data_disk_usage': 0, } ] return host_list @staticmethod def return_set_alertmanager_maintain_response(): maintain_info = MockResponse( {'status': 'success', 'data': {'silenceId': 'f9aed355-8a99-42ba-9fe6-877633ea3e4a'}}) return maintain_info @staticmethod def return_revoke_alertmanager_maintain_response(): revoke_maintain_info = MockResponse('') return revoke_maintain_info @mock.patch.object(requests, 'post', return_value='') def test_set_maintain_by_host_list(self, mock_post): mock_post.return_value = self.return_set_alertmanager_maintain_response() alertmanager = Alertmanager() maintain_ids = alertmanager.set_maintain_by_host_list( self.return_host_list()) TestCase.assertIsNotNone(maintain_ids, '添加维护失败') return maintain_ids @mock.patch.object(requests, 'post', return_value='') def test_set_maintain_by_env_name(self, mock_post): mock_post.return_value = self.return_set_alertmanager_maintain_response() alertmanager = Alertmanager() maintain_ids = alertmanager.set_maintain_by_env_name('default') TestCase.assertIsNotNone(maintain_ids, '添加维护失败') return maintain_ids @mock.patch.object(requests, 'post', return_value='') def test_revoke_alertmanager_maintain_by_host_list(self, mock_post): mock_post.return_value = self.return_revoke_alertmanager_maintain_response() alertmanager = Alertmanager() m1 = Maintain.objects.create( matcher_name="instance", matcher_value="10.0.3.71", maintain_id=1) m2 = Maintain.objects.create( matcher_name="instance", matcher_value="10.0.3.71", maintain_id=2) revoke_result = alertmanager.revoke_maintain_by_host_list( self.return_host_list()) TestCase.assertIsNotNone(revoke_result, '删除维护失败') m1.delete() m2.delete() @mock.patch.object(requests, 'post', return_value='') def test_revoke_alertmanager_maintain_by_env_name(self, mock_post): mock_post.return_value = self.return_revoke_alertmanager_maintain_response() alertmanager = Alertmanager() revoke_result = alertmanager.revoke_maintain_by_env_name('default') TestCase.assertIsNotNone(revoke_result, '删除维护失败') @mock.patch.object(requests, 'post', return_value='') def test_error_alertmanager_func1(self, mock_post): alertmanager = Alertmanager() result_format_time = alertmanager.format_time(1) self.assertIsNone(result_format_time) result_add_setting = alertmanager.add_setting( value=1, name='env', start_time=1, ends_time=2) self.assertIsNone(result_add_setting) result_add_setting = alertmanager.add_setting(value=1, name='env', start_time='2021-09-11 12:34:56', ends_time=2) self.assertIsNone(result_add_setting) mock_post.return_value = self.return_revoke_alertmanager_maintain_response() MonitorUrl.objects.filter(name='alertmanager').delete() alertmanager.get_alertmanager_config() def test_error_alertmanager_func2(self): alertmanager = Alertmanager() result_set_maintain_by_env_name = alertmanager.set_maintain_by_env_name( 'aaa') self.assertIsNone(result_set_maintain_by_env_name) result_revoke_maintain_by_host_list = alertmanager.revoke_maintain_by_host_list([ { 'ip': '10.0.3.73', 'data_folder': '/boot', 'cpu_usage': 0, 'mem_usage': 0, 'root_disk_usage': 0, 'data_disk_usage': 0, }]) self.assertEqual(result_revoke_maintain_by_host_list, False) def tearDown(self): MonitorUrl.objects.filter(name='alertmanager').delete() ================================================ FILE: omp_server/tests/test_promemonitor/test_celery_tasks.py ================================================ # -*- coding: utf-8 -*- # Project: test_celery_tasks # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 20:11 # IDE: PyCharm # Version: 1.0 # Introduction: from unittest import mock from tests.base import BaseTest from db_models.models import Host from utils.plugin.salt_client import SaltClient from promemonitor.tasks import monitor_agent_restart from promemonitor.tasks import real_monitor_agent_restart class MonitorAgentRestartCeleryTaskTest(BaseTest): """ 主机Agent的测试类 """ def setUp(self): super(MonitorAgentRestartCeleryTaskTest, self).setUp() self.correct_host_data = { "instance_name": "mysql_instance_1", "ip": "127.0.0.10", "port": 36000, "username": "root", "password": "uea_xeU_d_6YHCCY7Q-e2xZolSw2z2C3KGhLY6iMdnI", "data_folder": "/data", "operate_system": "CentOS", } self.host = Host(**self.correct_host_data) self.host.save() @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_restart_monitor_agent_success(self, *args, **kwargs): """ 测试重启主机Agent成功 :return: """ self.assertEqual(monitor_agent_restart(self.host.id), None) @mock.patch.object( SaltClient, "cmd", return_value=(False, "error_message")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_restart_agent_failed(self, *args, **kwargs): """ 测试重启主机Agent失败 :return: """ self.assertEqual(monitor_agent_restart(self.host.id), None) @mock.patch.object( SaltClient, "cmd", return_value=(False, "error_message")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_restart_agent_failed_with_wrong_id(self, *args, **kwargs): """ 测试重启主机Agent失败,主机id错误 :return: """ self.assertEqual(monitor_agent_restart(1000), None) @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_real_restart_monitor_agent_success(self, *args, **kwargs): """ 测试重启主机Agent成功 :return: """ self.assertEqual(real_monitor_agent_restart(self.host), None) @mock.patch.object( SaltClient, "cmd", return_value=(False, "error_message")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_real_restart_monitor_agent_failed(self, *args, **kwargs): """ 测试重启主机Agent失败 :return: """ self.assertEqual(real_monitor_agent_restart(self.host), None) ================================================ FILE: omp_server/tests/test_promemonitor/test_email_config.py ================================================ import json import os import shutil import requests import yaml from rest_framework.reverse import reverse from unittest import mock from tests.base import AutoLoginTest from db_models.models import EmailSMTPSetting, AlertSendWaySetting from omp_server.settings import PROJECT_DIR class MockResponse: """ 自定义mock response类 """ status = 200 def __init__(self, data): self.text = json.dumps(data) self.status_code = self.status def json(self): return json.loads(self.text) class EmailConfig(AutoLoginTest): @staticmethod def delete_conf_dir(): """ 删除alertmanager文件 :return: """ alertmanager_conf_dir = os.path.join( PROJECT_DIR, "component/alertmanager/conf") if os.path.exists(alertmanager_conf_dir): shutil.rmtree(alertmanager_conf_dir) def setUp(self): super(EmailConfig, self).setUp() self.delete_conf_dir() alertmanager_conf_dir = os.path.join( PROJECT_DIR, "component/alertmanager/conf") if not os.path.exists(alertmanager_conf_dir): os.makedirs(alertmanager_conf_dir) alertmanager_conf_dict = {"global": {"resolve_timeout": "5m", "smtp_from": "1jayden.liu@cloudwise.com", "smtp_smarthost": "smtp.feishu.cn:465", "smtp_auth_username": "jayden.liu@cloudwise.com", "smtp_auth_password": "Pc6qjfofl0TaqTlf", "smtp_require_tls": False, "smtp_hello": "qq.com"}, "templates": ["/data/omp/omp_monitor/promemonitor/alertmanager/templates/*tmpl"], "route": {"group_by": ["instance"], "group_wait": "10s", "group_interval": "10s", "repeat_interval": "1m", "receiver": "commonuser"}, "receivers": [ {"name": "commonuser", "email_configs": [ {"to": "lingyang.guo@cloudwise.com", "headers": {"Subject": "OMP ALERT"}, "html": "{{ template \"email.to.html\" . }}"}]}], "inhibit_rules": [ {"source_match": {"severity": "critical"}, "target_match": {"severity": "warning"}, "equal": ["instance", "job", "alertname"]}]} with open(os.path.join(alertmanager_conf_dir, "alertmanager.yml"), "w", encoding="utf8") as ay_fp: yaml.dump(alertmanager_conf_dict, ay_fp, allow_unicode=True) self.esc_get_url = reverse("getSendEmailConfig-list") self.esc_update_url = reverse("updateSendEmailConfig-list") self.rsc_get_url = reverse("getSendAlertSetting-list") self.rsc_update_url = reverse("updateSendAlertSetting-list") self.ess = EmailSMTPSetting.objects.create( email_host="123@qq.com", email_port="465", email_host_user="test_user", email_host_password="test_password" ) self.asws = AlertSendWaySetting.objects.create( used=True, env_id=1, way_name="email", server_url="123@qq.com", way_token="123", extra_info="" ) def tearDown(self): super(EmailConfig, self).tearDown() self.ess.delete() self.asws.delete() self.delete_conf_dir() def test_get_email_smtp_config(self): resp = self.get(self.esc_get_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch.object(requests, 'post', return_value=None) def test_update_smtp_config(self, mock_post=None): mock_post.return_value = MockResponse( {"status": "success", "status_code": 200}) post_data = { "host": "smtp.163.com", "port": 465, "username": "123456789@qq.com", "password": "12345" } resp = self.post(self.esc_update_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) def test_get_send_alert_config(self): resp = self.get(self.rsc_get_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch.object(requests, 'post', return_value=None) def test_update_send_alert_config(self, mock_post=None): mock_post.return_value = MockResponse( {"status": "success", "status_code": 200}) post_data = {"way_name": "email", "server_url": "98765432dd2@qq.com", "used": True, "env_id": 0} resp = self.post(self.rsc_update_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_promemonitor/test_global_maintain.py ================================================ import json import requests from rest_framework.reverse import reverse from unittest import mock from tests.base import AutoLoginTest from promemonitor.alertmanager import Alertmanager from db_models.models import MonitorUrl class MockResponse: """ 自定义mock response类 """ status = 200 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class GlobalMaintainTest(AutoLoginTest): """ 全局维护测试类 """ def setUp(self): super(GlobalMaintainTest, self).setUp() MonitorUrl.objects.create( name='alertmanager', monitor_url='127.0.0.1:19013') self.global_maintain_url = reverse("globalMaintain-list") # 正确数据 self.correct_maintain_data = { "matcher_name": "env", "matcher_value": "default" } @staticmethod def return_set_alertmanager_maintain_response(): maintain_info = MockResponse( {'status': 'success', 'data': {'silenceId': 'f9aed355-8a99-42ba-9fe6-877633ea3e4a'}}) return maintain_info @staticmethod def return_revoke_alertmanager_maintain_response(): revoke_maintain_info = MockResponse('') return revoke_maintain_info @mock.patch.object(requests, 'post', return_value='') def test_set_global_maintain(self, mock_post): """ 测试设置全局维护 """ mock_post.return_value = self.return_set_alertmanager_maintain_response() resp = self.post(self.global_maintain_url, self.correct_maintain_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) # print('已设置全局维护:', resp) # 删除主机 self.revoke_global_maintain(self.correct_maintain_data) @mock.patch.object(requests, 'delete', return_value='') def revoke_global_maintain(self, maintain_data, mock_post=''): mock_post.return_value = self.return_set_alertmanager_maintain_response() alertmanager = Alertmanager() alertmanager.revoke_maintain_by_env_name( env_name=maintain_data.get("matcher_value")) def tearDown(self): MonitorUrl.objects.filter(name='alertmanager').delete() ================================================ FILE: omp_server/tests/test_promemonitor/test_grafana_url.py ================================================ from unittest import mock from rest_framework.reverse import reverse from promemonitor.grafana_url import CurlPrometheus from tests.base import AutoLoginTest from tests.mixin import GrafanaMainPageResourceMixin # 正确prometheus数据 correct_prometheus_data = { "status": "success", "data": { "alerts": [ { "labels": { "alertname": "实例宕机", "instance": "10.0.3.72", "job": "nodeExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.72 已宕机超过1分钟", "summary": "实例宕机(10.0.3.72)" }, "state": "firing", "activeAt": "2021-09-27T07:08:05.68330499Z", "value": "0e+00" }, { "labels": { "alertname": "实例宕机", "instance": "10.0.3.73", "job": "nodeExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.71 已宕机超过1分钟", "summary": "实例宕机(10.0.3.71)" }, "state": "firing", "activeAt": "2021-09-27T07:07:24.495164774Z", "value": "0e+00" }, {'status': 'firing', 'labels': { 'alertname': 'app state', 'app': 'dolaLogMonitorServer', 'env': '118', 'instance': '10.0.7.164', 'job': 'dolaLogMonitorServerExporter', 'severity': 'critical' }, 'annotations': { 'consignee': 'cw-email-address', 'description': '主机 10.0.7.164 中的 服务 dolaLogMonitorServer 已经down掉超过一分钟.', 'summary': 'app state(instance 10.0.7.164)'}, 'startsAt': '2021-06-26T07:23:42.479972051Z', 'endsAt': '2021-06-26T07:38:01.343952065Z', 'generatorURL': 'https://centos7:19011/graph?g0.expr=probe_success+%3D%3D+0&g0.tab=1', 'fingerprint': '29a070a620efa300'} ] } } class GrafanaUrlTest(AutoLoginTest, GrafanaMainPageResourceMixin): def setUp(self): super(GrafanaUrlTest, self).setUp() self.list_grafana_url = reverse("grafanaurl-list") self.get_grafana_main_pages() @mock.patch.object(CurlPrometheus, "curl_prometheus", return_value=correct_prometheus_data) def test_exception_list(self, curl_prometheus): """请求全列表""" resp = self.get(self.list_grafana_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") @mock.patch.object(CurlPrometheus, "curl_prometheus", return_value=correct_prometheus_data) def test_exception_list_filter(self, curl_prometheus): """"多字段筛选并以单一字段排序""" data = {"ip": "10.0", "instance_name": "dola", "ordering": "ip"} resp = self.get(self.list_grafana_url, data=data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) ================================================ FILE: omp_server/tests/test_promemonitor/test_grafana_views.py ================================================ # -*- coding: utf-8 -*- # Project: test_grafana_views # Author: jon.liu@yunzhihui.com # Create time: 2021-10-12 10:55 # IDE: PyCharm # Version: 1.0 # Introduction: """ grafana views测试类 """ import requests from unittest import mock from tests.base import BaseTest from db_models.models import MonitorUrl class MockResponse(object): """ 自定义mock response类 """ def __init__(self, headers=None): self.content = "success" self.headers = headers self.status_code = 200 class TestGrafanaViews(BaseTest): def setUp(self): super(TestGrafanaViews, self).setUp() MonitorUrl.objects.create( name='grafana', monitor_url='127.0.0.1:19014') @mock.patch.object( requests, "request", return_value=MockResponse(headers=dict())) def test_success(self, request): res = self.get(url="/proxy/v1/grafana/") self.assertEqual(res.status_code, 200) @mock.patch.object( requests, "request", return_value=MockResponse(headers=None)) def test_failed_connected(self, request): res = self.get(url="/proxy/v1/grafana/") self.assertEqual(res.status_code, 200) ================================================ FILE: omp_server/tests/test_promemonitor/test_instance_name_list.py ================================================ from rest_framework.reverse import reverse from tests.base import AutoLoginTest class GetInstanceNameListTest(AutoLoginTest): """ 获取主机和应用实例名测试类 """ def setUp(self): super(GetInstanceNameListTest, self).setUp() self.instance_name_list_url = reverse("instanceNameList-list") def test_get_instance_name_list(self): """ 测试获取主机和应用实例名 """ resp = self.get(self.instance_name_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") ================================================ FILE: omp_server/tests/test_promemonitor/test_instrument_panel.py ================================================ import json from unittest import mock import requests from rest_framework.reverse import reverse from tests.base import AutoLoginTest from db_models.models import MonitorUrl class MockResponse: """ 自定义mock response类 """ status_code = 200 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class InstrumentPanelTest(AutoLoginTest): def setUp(self): super(InstrumentPanelTest, self).setUp() MonitorUrl.objects.create( name='prometheus', monitor_url='127.0.0.1:19011') self.instrument_panel_url = reverse("instrumentPanel-list") @staticmethod def return_prometheus_alerts_response(): prometheus_alerts_response = MockResponse( { "status": "success", "data": { "alerts": [ { "labels": { "alertname": "实例宕机", "instance": "10.0.3.71", "service_type": "host", "service_name": "node", "instance_name": "dosm1", "job": "nodeExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.71 已宕机超过1分钟", "summary": "实例宕机(10.0.3.71)" }, "state": "firing", "activeAt": "2021-10-16T08:16:24.495164774Z", "value": "0e+00" }, { "labels": { "alertname": "mysql down", "instance": "10.0.3.72", "service_type": "database", "service_name": "mysql", "instance_name": "mysql1", "job": "mysqlExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.72 已宕机超过1分钟", "summary": "实例宕机(10.0.3.72)" }, "state": "firing", "activeAt": "2021-10-16T08:16:05.68330499Z", "value": "0e+00" }, { "labels": { "alertname": "alertChanel down", "instance": "10.0.3.72", "service_type": "service", "service_name": "alertChanel", "instance_name": "alertChanel", "job": "alertChanelExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.72 已宕机超过1分钟", "summary": "实例宕机(10.0.3.72)" }, "state": "firing", "activeAt": "2021-10-16T08:16:05.68330499Z", "value": "0e+00" }, { "labels": { "alertname": "zookeeper down", "instance": "10.0.3.72", "service_type": "component", "service_name": "zookeeper", "instance_name": "alertChanel", "job": "zookeeperExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.72 已宕机超过1分钟", "summary": "实例宕机(10.0.3.72)" }, "state": "firing", "activeAt": "2021-10-16T08:16:05.68330499Z", "value": "0e+00" }, { "labels": { "alertname": "custom_kafka down", "instance": "10.0.3.75", "service_type": "third", "service_name": "kafka", "instance_name": "custom_kafka", "job": "kafkaExporter", "severity": "critical" }, "annotations": { "consignee": "987654321@qq.com", "description": "实例 10.0.3.72 已宕机超过1分钟", "summary": "实例宕机(10.0.3.72)" }, "state": "firing", "activeAt": "2021-10-16T08:16:05.68330499Z", "value": "0e+00" } ] } }) return prometheus_alerts_response @mock.patch.object(requests, 'get', return_value='') def test_instrument_panel(self, mock_get): mock_get.return_value = self.return_prometheus_alerts_response() resp = self.get(self.instrument_panel_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) def tearDown(self): super(InstrumentPanelTest, self).tearDown() ================================================ FILE: omp_server/tests/test_promemonitor/test_monitor_agent_restart.py ================================================ # -*- coding: utf-8 -*- # Project: test_monitor_agent_restart # Author: jon.liu@yunzhihui.com # Create time: 2021-10-09 11:42 # IDE: PyCharm # Version: 1.0 # Introduction: import random from unittest import mock from rest_framework.reverse import reverse from tests.base import AutoLoginTest from tests.mixin import HostsResourceMixin from promemonitor.tasks import monitor_agent_restart class MonitorAgentRestartTest(AutoLoginTest, HostsResourceMixin): """ 监控Agent重启测试类 """ def setUp(self): super(MonitorAgentRestartTest, self).setUp() self.restartMonitorAgent_url = reverse("restartMonitorAgent-list") @mock.patch.object(monitor_agent_restart, "delay", return_value=None) def test_success(self, monitor_agent_restart_obj): """ 请求成功测试 """ host_obj_ls = self.get_hosts(2) host_obj_id_ls = list(map(lambda x: x.id, host_obj_ls)) resp = self.post( self.restartMonitorAgent_url, data={"host_ids": host_obj_id_ls} ).json() self.assertDictEqual(resp, { "code": 0, "message": "success", "data": { "host_ids": host_obj_id_ls } }) self.destroy_hosts() @mock.patch.object(monitor_agent_restart, "delay", return_value=None) def test_failed(self, monitor_agent_restart_obj): """ 请求失败测试 """ self.get_hosts(2) resp = self.post( self.restartMonitorAgent_url, data={"host_ids": [random.randint(10000, 20000)]} ).json() self.assertEqual(resp.get("code"), 1) self.destroy_hosts() ================================================ FILE: omp_server/tests/test_promemonitor/test_promemonitor_url.py ================================================ from rest_framework.reverse import reverse from utils.parse_config import MONITOR_PORT from tests.base import AutoLoginTest from db_models.models import MonitorUrl class PromemonitorTest(AutoLoginTest): def setUp(self): super(PromemonitorTest, self).setUp() self.create_monitorurl_url = reverse("monitorurl-list") self.multiple_update = self.create_monitorurl_url + "multiple_update/" MonitorList = [] local_ip = "127.0.0.1:" MonitorList.append(MonitorUrl(id="1", name="prometheus", monitor_url=local_ip + str(MONITOR_PORT.get("prometheus", "19011")))) MonitorList.append(MonitorUrl(id="2", name="alertmanager", monitor_url=local_ip + str(MONITOR_PORT.get("alertmanager", "19013")))) MonitorList.append(MonitorUrl( id="3", name="grafana", monitor_url=local_ip + str(MONITOR_PORT.get("grafana", "19014")))) MonitorUrl.objects.bulk_create(MonitorList) def test_list_promeurl(self): """ 测试监控配置列表 """ # 查询配置列表 -> 查询成功 resp = self.get(self.create_monitorurl_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) def test_create_promeurl(self): # name名字重复 -> 无法创建 resp = self.post(self.create_monitorurl_url, { "name": "prometheus", "monitor_url": "127.0.0.1:8080", }).json() self.assertDictEqual(resp, { "code": 1, "message": "name已经存在", "data": None }) # name名字重复,批量创建 -> 无法创建 resp = self.post(self.create_monitorurl_url, {"data": [{ "name": "prometheus", "monitor_url": "127.0.0.1:8080" }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "name字段已经存在,detail:prometheus", "data": None }) # name名字空 -> 无法创建 resp = self.post(self.create_monitorurl_url, { "monitor_url": "127.0.0.1:8080", }).json() self.assertDictEqual(resp, { "code": 1, "message": "This field is required.", "data": None }) # name名字空,批量创建 -> 无法创建 resp = self.post(self.create_monitorurl_url, {"data": [{ "monitor_url": "127.0.0.1:8080", }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "name字段不为空", "data": None }) # name字段超限 -> 无法创建 resp = self.post(self.create_monitorurl_url, { "name": "prometheusprometheusprometheusprometheusprometheusprometheusprometheusprometheus", "monitor_url": "127.0.0.1:8080", }).json() self.assertDictEqual(resp, { "code": 1, "message": "Ensure this field has no more than 32 characters.", "data": None }) # name字段超限,批量创建 -> 无法创建 resp = self.post(self.create_monitorurl_url, {"data": [{ "name": "prometheusprometheusprometheusprometheusprometheusprometheusprometheusprometheus", "monitor_url": "127.0.0.1:8080", }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "name字段长度超过32,detail:prometheusprometheusprometheusprometheusprometheusprometheusprometheusprometheus", "data": None }) # monitor_url字段空 -> 无法创建 resp = self.post(self.create_monitorurl_url, { "name": "test1", }).json() self.assertDictEqual(resp, { "code": 1, "message": "This field is required.", "data": None }) # monitor_url字段空,批量 -> 无法创建 resp = self.post(self.create_monitorurl_url, {"data": [{ "name": "test1", }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "monitor_url是必须字段", "data": None }) # 创建成功 resp = self.post(self.create_monitorurl_url, { "name": "test1", "monitor_url": "127.0.0.1:8080", }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") # 成功批量 resp = self.post(self.create_monitorurl_url, {"data": [{ "name": "test3", "monitor_url": "127.0.0.1:8080" }, { "name": "test2", "monitor_url": "127.0.0.1:8080" } ]}).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") def test_partial_update_promeurl(self): # monitor_url字非法,批量 -> 无法修改 resp = self.patch(self.multiple_update, {"data": [{ "id": "3", "monitor_url": "😊" }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "监控地址url地址存在非法字符", "data": None }) # 修改url, -> 创建成功 resp = self.patch(self.multiple_update, {"data": [{ "id": "3", "monitor_url": "127.0.0.1:19999" }]}).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") # 修改url批量, -> 创建成功 resp = self.patch(self.multiple_update, {"data": [ { "id": "2", "monitor_url": "127.0.0.1:29999" }, { "id": "3", "monitor_url": "127.0.0.1:19999" }]}).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") ================================================ FILE: omp_server/tests/test_promemonitor/test_prometheus.py ================================================ import json import requests from django.test import TestCase from promemonitor.prometheus import Prometheus from db_models.models import MonitorUrl from unittest import mock from db_models.models import HostThreshold class MockResponse: """ 自定义mock response类 """ status_code = 200 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class TestPrometheus(TestCase): def setUp(self): MonitorUrl.objects.create( name='prometheus', monitor_url='127.0.0.1:19011') hts = list() for metric in ("cpu_used", "memory_used", "disk_root_used", "disk_data_used"): for level in ("warning", "critical"): hts.append(HostThreshold( index_type=metric, condition=">=", condition_value="80" if level == "warning" else "90", alert_level=level, # create_date="", env_id=1 )) HostThreshold.objects.bulk_create(hts) @staticmethod def return_host_list(): host_list = [ { 'ip': '10.0.3.71', 'data_folder': '/boot', } ] return host_list @staticmethod def return_host_info_data(): correct_host_info_data = [ {'ip': '10.0.3.71', 'data_folder': '/boot', 'cpu_usage': 12, 'mem_usage': 12, 'root_disk_usage': 12, 'data_disk_usage': 12, 'cpu_status': "normal", 'mem_status': "normal", 'root_disk_status': "normal", 'data_disk_status': "normal"} ] return correct_host_info_data request_get_response = {"status": "success", "data": {"resultType": "vector", "result": [ {"metric": {"instance": "10.0.3.71"}, "value": [ 1633782875.771, "11.360416666623973"]}, {"metric": {"instance": "10.0.3.72"}, "value": [ 1633782875.771, "11.04166666666666"]} ]}} error_request_get_response = {"status": "error", "data": {"resultType": "vector", "result": [ {"metric": {"instance": "10.0.3.71"}, "value": [ 1633782875.771, "11.360416666623973"]}, {"metric": {"instance": "10.0.3.72"}, "value": [ 1633782875.771, "11.04166666666666"]} ]}} @mock.patch.object(requests, 'get', return_value='') def test_get_prometheus_info(self, mock_post): mock_post.return_value = MockResponse(self.request_get_response) prometheus = Prometheus() result = prometheus.get_host_info(self.return_host_list()) # print(result) self.assertListEqual(result, self.return_host_info_data()) def test_get_host_metric_status(self): p = Prometheus() result_none = p.get_host_metric_status('cpu', None) self.assertIsNone(result_none) result_critical = p.get_host_metric_status('cpu', 91) self.assertEqual(result_critical, 'critical') result_warning = p.get_host_metric_status('cpu', 81) self.assertEqual(result_warning, 'warning') @mock.patch.object(requests, 'get', return_value='') def test_error_get_host_arg_usage(self, mock_get): mock_get.return_value = MockResponse(self.error_request_get_response) p = Prometheus() result_get_host_cpu_usage = p.get_host_cpu_usage( self.return_host_list()) self.assertIsNone(result_get_host_cpu_usage[0].get("cpu_usage")) result_get_host_mem_usage = p.get_host_mem_usage( self.return_host_list()) self.assertIsNone(result_get_host_mem_usage[0].get("mem_usage")) result_get_host_root_disk_usage = p.get_host_root_disk_usage( self.return_host_list()) self.assertIsNone( result_get_host_root_disk_usage[0].get("root_disk_usage")) result_get_host_data_disk_usage = p.get_host_data_disk_usage( self.return_host_list()) self.assertIsNone( result_get_host_data_disk_usage[0].get("data_disk_usage")) mock_get.return_value.status_code = -1 result_get_host_cpu_usage = p.get_host_cpu_usage( self.return_host_list()) self.assertIsNone(result_get_host_cpu_usage[0].get("cpu_usage")) result_get_host_mem_usage = p.get_host_mem_usage( self.return_host_list()) self.assertIsNone(result_get_host_mem_usage[0].get("mem_usage")) result_get_host_root_disk_usage = p.get_host_root_disk_usage( self.return_host_list()) self.assertIsNone( result_get_host_root_disk_usage[0].get("root_disk_usage")) result_get_host_data_disk_usage = p.get_host_data_disk_usage( self.return_host_list()) self.assertIsNone( result_get_host_data_disk_usage[0].get("data_disk_usage")) mock_get.return_value = MockResponse(self.request_get_response) mock_get.return_value.status_code = 200 result_get_host_cpu_usage = p.get_host_cpu_usage(1) self.assertEqual(result_get_host_cpu_usage, 1) result_get_host_mem_usage = p.get_host_mem_usage(1) self.assertEqual(result_get_host_mem_usage, 1) result_get_host_root_disk_usage = p.get_host_root_disk_usage(1) self.assertEqual(result_get_host_root_disk_usage, 1) result_get_host_data_disk_usage = p.get_host_data_disk_usage( [{"1": 1}, {"2": 2}]) self.assertEqual(result_get_host_data_disk_usage, [{'1': 1, 'data_disk_usage': None, 'data_disk_status': None}, {'2': 2, 'data_disk_usage': None, 'data_disk_status': None}]) def tearDown(self): MonitorUrl.objects.filter(name='prometheus').delete() HostThreshold.objects.filter(env_id=1).delete() ================================================ FILE: omp_server/tests/test_promemonitor/test_prometheus_utils.py ================================================ # -*- coding: utf-8 -*- # Project: test_prometheus_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-11 22:58 # IDE: PyCharm # Version: 1.0 # Introduction: """ prometheus 工具集的单元测试代码 """ import os import uuid import shutil import json from unittest import mock import requests from tests.base import BaseTest from omp_server.settings import PROJECT_DIR from promemonitor.prometheus_utils import PrometheusUtils class MockResponse: """ 自定义mock response类 """ status_code = 0 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class PrometheusUtilsTest(BaseTest): """ 主机Agent的测试类 """ @staticmethod def delete_conf_dir(): """ 删除prometheus文件 :return: """ conf_dir = os.path.join(PROJECT_DIR, "component/prometheus/conf") if os.path.exists(conf_dir): shutil.rmtree(conf_dir) def setUp(self): super(PrometheusUtilsTest, self).setUp() self.delete_conf_dir() self.prometheus_obj = PrometheusUtils() if not os.path.exists(self.prometheus_obj.prometheus_rules_path): os.makedirs(self.prometheus_obj.prometheus_rules_path) if not os.path.exists(self.prometheus_obj.prometheus_targets_path): os.makedirs(self.prometheus_obj.prometheus_targets_path) if not os.path.exists(self.prometheus_obj.prometheus_conf_path): with open(self.prometheus_obj.prometheus_conf_path, 'w') as pf: pf.write('') self.nodes_data = [ { "data_path": "/data", "env": "default", "ip": "127.0.0.1" } ] def test_init_success(self): """ 测试PrometheusUtils可正常实例化情况 :return: """ self.assertEqual( isinstance(self.prometheus_obj, PrometheusUtils), True) def test_add_node_failed_with_null_data(self): """ 测试add_node传入空数据情况 :return: """ flag, message = self.prometheus_obj.add_node([]) self.assertEqual(flag, False) self.assertEqual(message, "nodes_data can not be null") def test_add_node_success(self): """ 测试add_node成功的情况 :return: """ flag, message = self.prometheus_obj.add_node(self.nodes_data) self.assertEqual(flag, True) def test_add_node_success_2(self): """ 测试add_node成功的情况 :return: """ nodes_data = [ { "data_path": "/data1", "env": "default", "ip": "127.0.0.1" } ] self.prometheus_obj.add_node(self.nodes_data) flag, message = self.prometheus_obj.add_node(nodes_data) self.assertEqual(flag, True) def test_add_node_success_3(self): """ 测试add_node成功的情况 :return: """ nodes_data = [ { "data_path": "/data1", "env": "default", "ip": "127.0.0.1" } ] self.prometheus_obj.add_node(self.nodes_data) self.prometheus_obj.add_node(self.nodes_data) flag, message = self.prometheus_obj.add_node(nodes_data) self.assertEqual(flag, True) def test_delete_node_failed_with_null_data(self): """ 测试 delete_node 接收空数据 :return: """ self.assertEqual(self.prometheus_obj.delete_node([])[0], False) def test_delete_node_success(self): """ 测试 delete_node 成功 :return: """ self.test_add_node_success() flag, msg = self.prometheus_obj.delete_node(self.nodes_data) self.assertEqual(flag, True) def test_delete_node_failed_with_file_not_exist(self): """ 测试 delete_node 失败,target文件不存在 :return: """ flag, msg = self.prometheus_obj.delete_node(self.nodes_data) self.assertEqual(flag, False) def test_delete_node_success_with_empty_file(self): """ 测试 delete_node 失败,target文件不存在 :return: """ with open(self.prometheus_obj.node_exporter_targets_file, "w") as fp: fp.write("") flag, msg = self.prometheus_obj.delete_node(self.nodes_data) self.assertEqual(flag, True) def test_add_rules_failed(self): """ 测试 add_rules 失败 :return: """ self.assertEqual(self.prometheus_obj.add_rules("aaa")[0], False) def test_add_rules_for_node_success(self): """ 测试 add_rules 主机成功 :return: """ self.assertEqual(self.prometheus_obj.add_rules("node")[0], True) def test_add_rules_for_service_success(self): """ 测试 add_rules service成功 :return: """ self.assertEqual(self.prometheus_obj.add_rules("service")[0], True) def test_add_rules_for_exporter_success(self): """ 测试 add_rules exporter成功 :return: """ self.assertEqual(self.prometheus_obj.add_rules("exporter")[0], True) def test_delete_rules_failed(self): """ 测试 delete_rules 失败 :return: """ self.assertEqual(self.prometheus_obj.delete_rules("aaa")[0], False) def test_delete_rules_for_node_success(self): """ 测试 delete_rules node成功 :return: """ self.assertEqual(self.prometheus_obj.delete_rules("node")[0], True) def test_delete_rules_for_service_success(self): """ 测试 delete_rules status成功 :return: """ self.assertEqual(self.prometheus_obj.delete_rules("service")[0], True) def test_delete_rules_for_exporter_success(self): """ 测试 delete_rules exporter成功 :return: """ self.prometheus_obj.add_rules("exporter") self.assertEqual(self.prometheus_obj.delete_rules("exporter")[0], True) def test_replace_placeholder_failed(self): """ 测试文件不存在时无法替换placeholder :return: """ self.assertEqual( self.prometheus_obj.replace_placeholder( f"/tmp/{str(uuid.uuid4())}", [] )[0], False) @mock.patch.object(requests, 'post', return_value='') def test_add_service(self, mock_post): mock_post.return_value = MockResponse( {"return_code": 0, "message": "success"}) from db_models.models import Host h1 = Host.objects.create( instance_name="mysql_instance_1", ip="127.0.0.1", port=36000, username="root", password="XoIc56a3HiStUZb3Pu9jXEHj8YvMTRpMYnNFD2YS7MA", data_folder="/data", operate_system="CentOS" ) test_service_data = { "service_name": "mysql", "instance_name": "mysql_dosm", "data_path": "/data/appData/mysql", "log_path": "/data/logs/mysql", "env": "default", "ip": "127.0.0.1", "listen_port": "3306" } flag, msg = self.prometheus_obj.add_service(test_service_data) self.assertEqual(flag, True) h1.delete() @mock.patch.object(requests, 'post', return_value='') def test_delete_service(self, mock_post): mock_post.return_value = MockResponse( {"return_code": 0, "message": "success"}) mysql_json_file = os.path.join( self.prometheus_obj.prometheus_targets_path, 'mysqlExporter_all.json') with open(mysql_json_file, 'w') as mp: mp.write('') from db_models.models import Host h2 = Host.objects.create( instance_name="mysql_instance_1", ip="127.0.0.1", port=36000, username="root", password="XoIc56a3HiStUZb3Pu9jXEHj8YvMTRpMYnNFD2YS7MA", data_folder="/data", operate_system="CentOS" ) test_service_data = { "service_name": "mysql", "instance_name": "mysql_dosm", "data_path": "/data/appData/mysql", "log_path": "/data/logs/mysql", "env": "default", "ip": "127.0.0.1", "listen_port": "3306" } flag, msg = self.prometheus_obj.delete_service(test_service_data) self.assertEqual(flag, True) h2.delete() def tearDown(self) -> None: """ 测试结束操作 :return: """ self.delete_conf_dir() ================================================ FILE: omp_server/tests/test_promemonitor/test_receive_alert.py ================================================ import json from rest_framework.reverse import reverse from tests.base import AutoLoginTest from db_models.models import Host, Alert class MockResponse: """ 自定义mock response类 """ status_code = 0 def __init__(self, data): self.text = json.dumps(data) def json(self): return json.loads(self.text) class ReceiveAlertTest(AutoLoginTest): """ 接收并解析alertmanager告警测试类 """ def setUp(self): super(ReceiveAlertTest, self).setUp() self.receive_alert_url = reverse("receiveAlert-list") # 正确请求数据 self.origin_alert_str = { "receiver": "cloudwise", "status": "firing", "alerts": [ { "status": "firing", "labels": { "alertname": "host cpu_used critical alert", "instance": "10.0.7.146", "job": "nodeExporter", "severity": "critical" }, "annotations": { "consignee": "123456789@qq.com", "description": "主机 10.0.7.146 CPU 使用率为 10.06%, 大于阈值 10", "summary": "cpu_used (instance 10.0.7.146)" }, "startsAt": "2021-06-26T08:13:32.950510932Z", "endsAt": "2021-06-26T08:15:02.950510932Z", "generatorURL": "http://centos7:19011/graph?g0.expr=sum+by%28instance%29+%28avg+without%28cpu%29+" "%28irate%28node_cpu_seconds_total%7Benv%3D%22caleb%22%2Cmode%21%3D%22idle%22%7D%5B" "5m%5D%29%29%29+%2A+100+%3E%3D+10&g0.tab=1", "fingerprint": "3e16190fffa56fe0" }, { "status": "firing", "labels": { "alertname": "host cpu_used critical alert", "instance": "10.0.9.62", "job": "nodeExporter", "severity": "critical" }, "annotations": { "consignee": "123456789@qq.com", "description": "主机 10.0.9.62 CPU 使用率为 10.11%, 大于阈值 10", "summary": "cpu_used (instance 10.0.7.146)" }, "startsAt": "2021-06-26T08:13:32.950510932Z", "endsAt": "2021-06-26T08:15:02.950510932Z", "generatorURL": "http://centos7:19011/graph?g0.expr=sum+by%28instance%29+%28avg+without%28cpu%29+" "%28irate%28node_cpu_seconds_total%7Benv%3D%22caleb%22%2Cmode%21%3D%22idle%22%7D%5B" "5m%5D%29%29%29+%2A+100+%3E%3D+10&g0.tab=1", "fingerprint": "3e16190fffa56fe0" }, {'status': 'firing', 'labels': {'alertname': 'app state', 'app': 'alertChannel', 'env': '118', 'instance': '10.0.7.146', 'job': 'alertChannelExporter', 'severity': 'critical'}, 'annotations': {'consignee': 'cw-email-address', 'description': '主机 10.0.7.146 中的 服务 alertChannel 已经down掉超过一分钟.', 'summary': 'app state(instance 10.0.7.166)'}, 'startsAt': '2021-06-26T06:45:31.343952065Z', 'endsAt': '0001-01-01T00:00:00Z', 'generatorURL': 'http://centos7:19011/graph?g0.expr=probe_success+%3D%3D+0&g0.tab=1', 'fingerprint': '941445cf659314a2' }, {'status': 'firing', 'labels': {'alertname': 'app state', 'app': 'cmdbServer', 'env': '118', 'instance': '10.0.9.61', 'job': 'cmdbServerExporter', 'severity': 'critical'}, 'annotations': {'consignee': 'cw-email-address', 'description': '主机 10.0.9.61 中的 服务 cmdbServer 已经down掉超过一分钟.', 'summary': 'app state(instance 10.0.7.166)'}, 'startsAt': '2021-06-26T06:45:31.343952065Z', 'endsAt': '0001-01-01T00:00:00Z', 'generatorURL': 'http://centos7:19011/graph?g0.expr=probe_success+%3D%3D+0&g0.tab=1', 'fingerprint': '941445cf659314a2' } ] } Host.objects.create( instance_name="mysql_instance_1", ip="10.0.7.146", port=36000, username="root", password="XoIc56a3HiStUZb3Pu9jXEHj8YvMTRpMYnNFD2YS7MA", data_folder="/data", operate_system="CentOS" ) request_post_response = { "code": 0, "message": "success", "data": {'key': ['...']} } def test_receive_alerts(self): """ 接收并解析alertmanager告警 """ # mock_post.return_value = MockResponse(self.request_post_response) resp = self.post(self.receive_alert_url, self.origin_alert_str).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get('data')) def test_alert_util_exception(self): """测试alert_util中的异常处理""" from promemonitor.alert_util import get_monitor_url, get_log_url, utc_to_local gmu_result = get_monitor_url(1) self.assertIsNone(gmu_result) glu_result = get_log_url(1) self.assertIsNone(glu_result) utl_result = utc_to_local("123") self.assertIsNotNone(utl_result) def tearDown(self): Alert.objects.filter(alert_host_ip='10.0.7.146').delete() Host.objects.filter(ip='10.0.7.146').delete() ================================================ FILE: omp_server/tests/test_promemonitor/test_threshold_rw.py ================================================ import json import os import shutil import requests from rest_framework.reverse import reverse from unittest import mock from db_models.models import HostThreshold, ServiceThreshold, ServiceCustomThreshold from tests.base import AutoLoginTest from omp_server.settings import PROJECT_DIR class MockResponse: """ 自定义mock response类 """ status = 200 def __init__(self, data): self.text = json.dumps(data) self.status_code = self.status def json(self): return json.loads(self.text) class ThresholdRW(AutoLoginTest): @staticmethod def delete_conf_dir(): """ 删除prometheus文件 :return: """ prometheus_conf_dir = os.path.join( PROJECT_DIR, "component/prometheus/conf") if os.path.exists(prometheus_conf_dir): shutil.rmtree(prometheus_conf_dir) def setUp(self): super(ThresholdRW, self).setUp() self.delete_conf_dir() prometheus_conf_dir = os.path.join( PROJECT_DIR, "component/prometheus/conf") prometheus_rules_dir = os.path.join(prometheus_conf_dir, "rules") if not os.path.exists(prometheus_rules_dir): os.makedirs(prometheus_rules_dir) default_node_yml_file = os.path.join( prometheus_rules_dir, "default_node_rule.yml") with open(default_node_yml_file, 'w') as fp: fp.write("default_node_yml_dict") self.host_threshold_rw_url = reverse("hostThreshold-list") self.service_threshold_rw_url = reverse("serviceThreshold-list") self.custom_threshold_rw_url = reverse("customThreshold-list") self.host_threshold = HostThreshold.objects.create( index_type="cpu_used", condition=">=", condition_value="90", alert_level="critical", env_id=1 ) self.service_threshold = ServiceThreshold.objects.create( index_type="cpu_used", condition=">=", condition_value="90", alert_level="critical", env_id=1 ) self.custom_threshold = ServiceCustomThreshold.objects.create( service_name="kafka", index_type="cpu_used", condition=">=", condition_value="90", alert_level="critical", env_id=1 ) def tearDown(self): super(ThresholdRW, self).tearDown() self.delete_conf_dir() self.host_threshold.delete() self.service_threshold.delete() self.custom_threshold.delete() def test_get_host_threshold_config(self): resp = self.get(f"{self.host_threshold_rw_url}{'?env_id=1'}").json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch.object(requests, 'post', return_value=None) def test_update_host_threshold_config(self, mock_post=None): post_data = { "update_data": { "cpu_used": [ { "index_type": "cpu_used", "condition": ">=", "value": 91, "level": "critical" }, { "index_type": "cpu_used", "condition": ">=", "value": "80", "level": "warning" }]}, "env_id": 1} resp = self.post(self.host_threshold_rw_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) def test_get_service_threshold_config(self): resp = self.get(f"{self.service_threshold_rw_url}{'?env_id=1'}").json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch.object(requests, 'post', return_value=None) def test_update_service_threshold_config(self, mock_post=None): post_data = { "update_data": { "cpu_used": [ { "index_type": "cpu_used", "condition": ">=", "value": 91, "level": "critical" }, { "index_type": "cpu_used", "condition": ">=", "value": "80", "level": "warning" }]}, "env_id": 1} resp = self.post(self.service_threshold_rw_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) def test_get_custom_threshold_config(self): resp = self.get(f"{self.custom_threshold_rw_url}{'?env_id=1'}").json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) @mock.patch.object(requests, 'post', return_value=None) def test_update_custom_threshold_config(self, mock_post=None): post_data = { "env_id": 1, "service_name": "kafka", "index_type": "kafka_consumergroup_lag", "index_type_info": [ { "condition": ">=", "index_type": "kafka_consumergroup_lag", "level": "critical", "value": 5000 }, { "condition": ">=", "index_type": "kafka_consumergroup_lag", "level": "warning", "value": 3000 } ] } resp = self.post(self.custom_threshold_rw_url, data=post_data).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_services/__init__.py ================================================ ================================================ FILE: omp_server/tests/test_services/test_service_actions.py ================================================ import json from rest_framework.reverse import reverse from db_models.models import ( ServiceHistory, Service, Env, ApplicationHub, DetailInstallHistory ) from tests.base import AutoLoginTest from tests.mixin import ( ServicesResourceMixin ) from services.tasks import exec_action from unittest import mock import time install_detail = { "ip": "10.0.7.184", "name": "mysql", "version": "5.7.31", "app_port": [{"key": "service_port", "name": "服务端口", "default": "3306"}], "deploy_mode": {"key": "single", "name": "单实例"}, "install_args": [ {"key": "base_dir", "name": "安装目录", "default": "/webber/mysql", "dir_key": "{data_path}", "check_msg": "success", "check_flag": "true"}, {"key": "data_dir", "name": "数据目录", "default": "/webber/mysql/data", "dir_key": "{data_path}", "check_msg": "success", "check_flag": "true"}, {"key": "log_dir", "name": "日志目录", "default": "/webber/mysql/log", "dir_key": "{data_path}", "check_msg": "success", "check_flag": "true"}, {"key": "username", "name": "用户名", "default": "root"}, {"key": "password", "name": "密码", "default": "123456"}], "service_instance_name": "mysql-7-184" } class ListActionTest(AutoLoginTest, ServicesResourceMixin): """ 服务动作测试类 """ def setUp(self): super(ListActionTest, self).setUp() env_obj = Env.objects.create(name="default") app_obj = ApplicationHub.objects.create( app_name="test_app", app_version="1.0.0", app_dependence=json.dumps( [{"name": "jda", "version": "1.0.0"}] ) ) Service.objects.create( ip="192.168.0.110", service_instance_name="test1", service_status=5, alert_count=6, self_healing_count=6, service_controllers={"start": "1.txt", "stop": "2.txt"}, env=env_obj, service=app_obj, service_port=json.dumps([{'default': '18080', 'key': 'http_port'}]) ) self.create_action_url = reverse("action-list") Service.objects.create( ip="192.168.0.111", service_instance_name="test2-jdk", service_status=5, alert_count=6, self_healing_count=6, service_controllers={"start": "1.txt", "stop": "2.txt"}, service=app_obj, service_port=json.dumps([{'default': '18080', 'key': 'http_port'}]) ) @mock.patch( "utils.plugin.salt_client.SaltClient.cmd", return_value=(True, "success")) def test_service_action_true(self, status): service_obj = Service.objects.get(ip="192.168.0.110") exec_action("1", service_obj.id, "admin") history_count = ServiceHistory.objects.filter( service=service_obj).count() service_obj.refresh_from_db() res = {service_obj.service_status: history_count} self.assertDictEqual(res, { 0: 1 }) @mock.patch( "utils.plugin.salt_client.SaltClient.cmd", return_value=(False, "false")) def test_service_action_false(self, status): service_obj = Service.objects.get(ip="192.168.0.110") exec_action("1", service_obj.id, "admin") history_count = ServiceHistory.objects.filter( service=service_obj).count() service_obj.refresh_from_db() res = {service_obj.service_status: history_count} self.assertDictEqual(res, { 4: 1 }) @mock.patch( "utils.plugin.salt_client.SaltClient.cmd", return_value="") @mock.patch( "promemonitor.prometheus_utils.PrometheusUtils.delete_service", return_value="" ) @mock.patch( "utils.plugin.salt_client.SaltClient", return_value="") def test_service_action_delete(self, salt_client, delete_service, status): status.side_effect = [ (True, "success"), (True, "success"), (True, "success") ] service_obj = Service.objects.get(ip="192.168.0.110") time_array = time.localtime(int(time.time())) time_style = time.strftime("%Y-%m-%d %H:%M:%S", time_array) service_history = ServiceHistory( username='admin', description='测试', result=0, created=time_style, service=service_obj ) service_history.save() DetailInstallHistory.objects.create( service=service_obj, send_msg="send_log", unzip_msg="unzip_log", install_msg="install_log", init_msg="init_log", start_msg="start_log", install_detail_args=install_detail ) exec_action("4", service_obj.id, "admin") history_count = ServiceHistory.objects.filter( service=service_obj).count() new_service = Service.objects.filter(ip="192.168.0.110").count() self.assertEqual(history_count, 0) self.assertEqual(new_service, 0) @mock.patch("services.tasks.exec_action.delay", return_value=True) def test_service_action_post(self, tasks): # 参数正常 -> 成功 resp = self.post(self.create_action_url, {"data": [{ "action": "1", "id": "1", "operation_user": "admin", }]}).json() self.assertEqual(resp.get("code"), 0) # 参数缺失 -> 失败 resp = self.post(self.create_action_url, {"data": [{ "action": "1", }]}).json() self.assertDictEqual(resp, { "code": 1, "message": "请输入action或id", "data": None }) def test_service_delete_post(self): # 参数正常 -> 成功 create_delete_url = reverse("delete-list") service_obj = Service.objects.get(ip="192.168.0.110") resp = self.post(create_delete_url, {"data": [{ "action": "1", "id": str(service_obj.id), "operation_user": "admin", }]}).json() self.assertEqual(resp.get("code"), 0) ================================================ FILE: omp_server/tests/test_services/test_services.py ================================================ import random from rest_framework.reverse import reverse from db_models.models import Service from tests.base import AutoLoginTest from tests.mixin import ( LabelsResourceMixin, ServicesResourceMixin ) class ListServiceTest(AutoLoginTest, ServicesResourceMixin): """ 服务列表测试类 """ def setUp(self): super(ListServiceTest, self).setUp() self.list_service_url = reverse("services-list") self.service_ls = self.get_services() def tearDown(self): super(ListServiceTest, self).tearDown() self.destroy_services() def test_services_list_filter(self): """ 测试服务列表过滤 """ # 查询服务列表 -> 展示所有非基础环境服务 resp = self.get(self.list_service_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( resp.get("data").get("count"), self.service_ls.filter( service__is_base_env=False).count() ) # 所有服务冗余字段 '亲和力' 为 tengine 的字段,视为前端服务,状态设置为 '正常' res_ls = resp.get("data").get("results") for service in res_ls: if service.get("is_web"): self.assertNotEqual(service.get("service_status"), "未监控") # IP 过滤 -> 模糊匹配 ip_field = str(random.randint(1, 20)) resp = self.get(self.list_service_url, { "ip": ip_field }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( resp.get("data").get("count"), Service.objects.filter( service__is_base_env=False, ip__contains=ip_field).count() ) # 服务实例名称过滤 -> 模糊匹配 name_field = str(random.randint(1, 20)) resp = self.get(self.list_service_url, { "service_instance_name": name_field }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( resp.get("data").get("count"), self.service_ls.filter( service__is_base_env=False, service_instance_name__contains=name_field).count() ) # 功能模块过滤 -> 精确匹配 label_field = f"{LabelsResourceMixin.LABEL_NAME_START}_{random.randint(1, 10)}" resp = self.get(self.list_service_url, { "label_name": label_field }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( resp.get("data").get("count"), Service.objects.filter( service__is_base_env=False, service__app_labels__label_name=label_field).count() ) # 服务类型过滤 -> 精确匹配 app_type = random.choice((0, 1)) resp = self.get(self.list_service_url, { "app_type": app_type }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertEqual( resp.get("data").get("count"), Service.objects.filter( service__is_base_env=False, service__app_type=app_type).count() ) def test_services_list_order(self): """ 测试服务列表排序 """ # 不传递排序字段 resp = self.get(self.list_service_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIsNotNone(resp.get("data")) # 传递排序字段,按照指定字段排序 reverse_flag = random.choice(("", "-")) order_field = "service_instance_name" ordering = f"{reverse_flag}{order_field}" resp = self.get(self.list_service_url, { "ordering": ordering }).json() instance_name_ls = list(map( lambda x: x.get("service_instance_name"), resp.get("data").get("results")) ) target_instance_name_ls = list( self.service_ls.filter( service__is_base_env=False).order_by( ordering).values_list( "service_instance_name", flat=True))[:10] self.assertEqual(instance_name_ls, target_instance_name_ls) class ServiceDetailTest(AutoLoginTest, ServicesResourceMixin): """ 服务详情测试类 """ def setUp(self): super(ServiceDetailTest, self).setUp() self.service_ls = self.get_services() def tearDown(self): super(ServiceDetailTest, self).tearDown() self.destroy_services() def test_service_detail(self): """ 测试服务详情 """ # 使用不存在 id -> 未找到 resp = self.get(reverse("services-detail", [9999])).json() self.assertDictEqual(resp, { 'code': 1, 'message': '未找到', 'data': None }) # 使用存在 id -> 查询成功 resp = self.get(reverse("services-detail", [ random.choice(self.service_ls).id ])).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_users/__init__.py ================================================ ================================================ FILE: omp_server/tests/test_users/test_login.py ================================================ import datetime from rest_framework.reverse import reverse from tests.base import BaseTest class LoginTest(BaseTest): """ 登录功能测试类 """ def setUp(self): super(LoginTest, self).setUp() self.login_url = reverse("login") self.users_list_url = reverse("users-list") @staticmethod def get_interval_time(gmt_time): """ 获取间隔时间 """ expiration_time = datetime.datetime.strptime( gmt_time, "%a, %d %b %Y %H:%M:%S GMT" ) + datetime.timedelta(hours=8) expiration_time = expiration_time - datetime.datetime.now() # 加一分钟,避免时间损耗影响计算精度 return expiration_time + datetime.timedelta(minutes=1) def test_login(self): """ 测试用户登录 """ # 不提供用户名、密码 -> 登录失败 resp = self.post(self.login_url, {}).json() self.assertDictEqual(resp, { "code": 1, "message": "Unable to log in with provided credentials.", "data": None, }) # 用户名、密码为空 -> 登录失败 resp = self.post(self.login_url, { "username": " ", "password": self.default_user.password, }).json() self.assertDictEqual(resp, { "code": 1, "message": "Unable to log in with provided credentials.", "data": None, }) # 用户名错误 -> 登录失败 resp = self.post(self.login_url, { "username": "wrong_user", "password": self.default_user.password, }).json() self.assertDictEqual(resp, { "code": 1, "message": "Unable to log in with provided credentials.", "data": None, }) # 密码错误 -> 登录失败 resp = self.post(self.login_url, { "username": self.default_user.username, "password": "wrong_password", }).json() self.assertDictEqual(resp, { "code": 1, "message": "Unable to log in with provided credentials.", "data": None, }) # 用户名、密码错误 -> 登录失败 resp = self.post(self.login_url, { "username": "wrong_user", "password": "wrong_password", }).json() self.assertDictEqual(resp, { "code": 1, "message": "Unable to log in with provided credentials.", "data": None, }) # 用户名、密码正确 -> 登录成功,生成 token 令牌 resp = self.post(self.login_url, { "username": self.default_user.username, "password": self.default_user.password, }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertIn("token", resp.get("data")) def test_access_api(self): """ 测试访问 API """ # 未登录用户 -> 无法访问,提示 "未认证" resp = self.get(self.users_list_url).json() self.assertDictEqual(resp, { "code": 1, "message": "未认证", "data": None, }) # 已登录用户 -> 允许访问 self.login() resp = self.get(self.users_list_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") # 退出登录 -> 无法访问,提示 "未认证" self.logout() resp = self.get(self.users_list_url).json() self.assertDictEqual(resp, { "code": 1, "message": "未认证", "data": None, }) def test_jwt_expiration(self): """ 测试 jwt 过期时间 """ # 登录 (默认) -> jwt 过期时间 1 天 self.login() gmt_time = self.client.cookies.get("jwtToken").get("expires") self.assertEqual(self.get_interval_time(gmt_time).days, 1) self.logout() # 登录 (记住密码) -> jwt 过期时间 7 天 self.login(remember=True) gmt_time = self.client.cookies.get("jwtToken").get("expires") self.assertEqual(self.get_interval_time(gmt_time).days, 7) self.logout() ================================================ FILE: omp_server/tests/test_users/test_users.py ================================================ from rest_framework.reverse import reverse from tests.base import AutoLoginTest from db_models.models import UserProfile class UsersTest(AutoLoginTest): """ 用户功能测试类 """ @staticmethod def get_user(): """ 获取用户 """ user = UserProfile.objects.create_user( username="test_user", password="test_user", email="test_user@cloudwise.com", ) return user @staticmethod def destroy_user(): """ 销毁用户 """ UserProfile.objects.filter( username="test_user").delete() def setUp(self): super(UsersTest, self).setUp() self.create_user_url = reverse("users-list") self.list_user_url = reverse("users-list") def test_create_user(self): """ 测试创建用户 """ # 已存在用户名 -> 无法创建 resp = self.post(self.create_user_url, { "username": self.default_user.username, "password": self.default_user.password, "re_password": self.default_user.password, "email": self.default_user.email, }).json() self.assertDictEqual(resp, { 'code': 1, 'message': '用户名已存在', 'data': None }) # 两次密码不一致 -> 无法创建 resp = self.post(self.create_user_url, { "username": "new_user", "password": "new_password", "re_password": "diff_password", "email": "user@cloudwise.com", }).json() self.assertDictEqual(resp, { "code": 1, "message": "两次密码不一致", "data": None }) # 邮箱格式不正确 -> 无法创建 resp = self.post(self.create_user_url, { "username": "new_user", "password": "new_password", "re_password": "diff_password", "email": "this is a email", }).json() self.assertDictEqual(resp, { "code": 1, "message": "邮箱格式不正确", "data": None }) # 全新用户名,两次密码一致 -> 创建成功 resp = self.post(self.create_user_url, { "username": "new_user", "password": "new_password", "re_password": "new_password", "email": "user@cloudwise.com", }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) def test_list_user(self): """ 测试查询用户列表 """ # 查询用户列表 -> 查询成功 resp = self.get(self.create_user_url).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) def test_retrieve_user(self): """ 测试查询一个用户 """ user = self.get_user() # 查询不存在用户 -> 查询失败 resp = self.get(reverse("users-detail", [9999])).json() self.assertDictEqual(resp, { 'code': 1, 'message': '未找到', 'data': None }) # 查询存在用户 -> 查询成功 resp = self.get(reverse("users-detail", [user.id])).json() user_info = resp.get("data", None) self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(user_info is not None) self.assertEqual(user_info.get("username"), user.username) self.assertNotEqual(user_info.get("password"), user.password) self.assertEqual(user_info.get("email"), user.email) self.destroy_user() def test_update_user(self): """ 测试更新一个已有用户 """ user = self.get_user() # 更新不存在用户 -> 更新失败 resp = self.put(reverse("users-detail", [9999]), { "username": user.username, "password": "update_user_pass", "re_password": "update_user_pass", "email": "update_user@cloudwise.com", }).json() self.assertDictEqual(resp, { 'code': 1, 'message': '未找到', 'data': None }) # 更新已有用户,密码不一致 -> 更新失败 resp = self.put(reverse("users-detail", [user.id]), { "username": user.username, "password": "update_user_pass", "re_password": "update_user_pass_diff", "email": "update_user@cloudwise.com", }).json() self.assertDictEqual(resp, { "code": 1, "message": "两次密码不一致", "data": None }) # 更新已有用户,两次密码一致 -> 更新成功 new_email = "update_user_email@cloudwise.com" resp = self.put(reverse("users-detail", [user.id]), { "username": user.username, "password": "update_user_pass", "re_password": "update_user_pass", "email": new_email, }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) self.destroy_user() def test_partial_update_user(self): """ 测试更新一个现有用户的一个或多个字段 """ user = self.get_user() # 更新不存在用户 -> 更新失败 resp = self.patch(reverse("users-detail", [9999]), { "password": "partial_update_user_pass", "re_password": "partial_update_user_pass", "email": "partial_update_user_email@cloudwise.com", }).json() self.assertDictEqual(resp, { 'code': 1, 'message': '未找到', 'data': None }) # 更新存在用户 -> 更新成功 new_email = "partial_update_user_email@cloudwise.com" resp = self.patch(reverse("users-detail", [user.id]), { "password": "new_password_one", "re_password": "new_password_one", "email": new_email, }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data", None) is not None) self.destroy_user() def test_delete_user(self): """ 测试删除一个现有用户 """ self.get_user() # 删除不存在用户 -> 删除失败 resp = self.delete(reverse("users-detail", [9999])).json() self.assertDictEqual(resp, { 'code': 1, 'message': '未找到', 'data': None }) self.destroy_user() class UserUpdatePasswordTest(AutoLoginTest): """ 用户更新密码测试类 """ def setUp(self): super(UserUpdatePasswordTest, self).setUp() self.update_password_url = reverse("updatePassword-list") def test_update_password(self): """ 测试更新密码 """ # 原密码错误 -> 更新失败 resp = self.post(self.update_password_url, { "username": self.default_user.username, "old_password": "error_password", "new_password": "new_password" }).json() self.assertDictEqual(resp, { "code": 1, "message": "当前密码不正确", "data": None }) # 新密码包含中文特殊符号 -> 更新失败 resp = self.post(self.update_password_url, { "username": self.default_user.username, "old_password": "error_password", "new_password": "zh。~password" }).json() self.assertDictEqual(resp, { "code": 1, "message": "新密码格式不合法", "data": None }) # 原密码正确 -> 更新成功 resp = self.post(self.update_password_url, { "username": self.default_user.username, "old_password": self.default_user.password, "new_password": "new_password" }).json() self.assertEqual(resp.get("code"), 0) self.assertEqual(resp.get("message"), "success") self.assertTrue(resp.get("data") is not None) ================================================ FILE: omp_server/tests/test_utils/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 10:24 # IDE: PyCharm # Version: 1.0 # Introduction: ================================================ FILE: omp_server/tests/test_utils/test_agent_util.py ================================================ # -*- coding: utf-8 -*- # Project: test_agent_util # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 10:25 # IDE: PyCharm # Version: 1.0 # Introduction: """ 主机Agent使用的测试代码 """ import os import shutil from unittest import mock from tests.base import BaseTest from utils.plugin.ssh import SSH from utils.plugin.agent_util import Agent from omp_server.settings import PROJECT_DIR class AgentUtilTest(BaseTest): """ 主机Agent的测试类 """ def setUp(self): super(AgentUtilTest, self).setUp() self.agent = Agent( host="127.0.0.1", port=22, username="root", password="root", install_dir="/data" ) _test_conf_path = os.path.join(PROJECT_DIR, "package_hub/127.0.0.1") if os.path.exists(_test_conf_path): shutil.rmtree(_test_conf_path) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value=(True, "success")) def test_deploy_agent_success(self, check, cmd, file_push): """ 测试成功部署 :return: """ self.assertEqual(self.agent.agent_deploy()[0], True) @mock.patch.object(SSH, "check", return_value=(False, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value=(True, "success")) def test_deploy_agent_false_ssh(self, check, cmd, file_push): """ 测试ssh连接失败 :return: """ self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) @mock.patch.object(SSH, "file_push", return_value=(True, "success")) def test_deploy_agent_false_cmd(self, check, cmd, file_push): """ 测试命令执行失败 :return: """ self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value=(False, "failed")) def test_deploy_agent_false_agent_file(self, check, cmd, file_push): """ 测试agent文件推送失败 :return: """ self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value="") def test_deploy_agent_false_file_config(self, file_push, cmd, check): """ 测试配置文件推送失败 :return: """ file_push.side_effect = [ (True, "success"), (False, "error"), (True, "success")] self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value="") def test_deploy_agent_false_file_script(self, file_push, cmd, check): """ 测试Agent脚本文件推送失败 :return: """ file_push.side_effect = [ (True, "success"), (True, "success"), (False, "error")] self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value="") @mock.patch.object(SSH, "file_push", return_value=(True, "success")) def test_deploy_agent_false_start(self, file_push, cmd, check): """ 测试Agent启动失败 :return: """ cmd.side_effect = [(True, "success"), (True, "success"), (False, "error")] self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value="") @mock.patch.object(SSH, "file_push", return_value=(True, "success")) def test_deploy_agent_success_start(self, file_push, cmd, check): """ 测试Agent启动成功 :return: """ cmd.side_effect = [ (True, "success"), (True, "success"), (False, "INIT_OMP_SALT_AGENT_SUCCESS") ] self.assertEqual(self.agent.agent_deploy()[0], True) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) @mock.patch.object(SSH, "file_push", return_value=(True, "success")) @mock.patch.object(Agent, "generate_conf", return_value=(False, "success")) def test_deploy_agent_false_generate_conf(self, check, cmd, file_push, generate_conf): """ 测试生成配置文件报错 :return: """ self.assertEqual(self.agent.agent_deploy()[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_agent_start_success(self, cmd, check): """ 测试启动成功 :return: """ self.assertEqual(self.agent.agent_manage( "start", "/data/omp_salt_agent")[0], True) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) def test_agent_start_failed(self, cmd, check): """ 测试启动失败 :return: """ self.assertEqual(self.agent.agent_manage( "start", "/data/omp_salt_agent")[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_agent_stop_success(self, cmd, check): """ 测试停止成功 :return: """ self.assertEqual(self.agent.agent_manage( "stop", "/data/omp_salt_agent")[0], True) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) def test_agent_stop_failed(self, cmd, check): """ 测试停止失败 :return: """ self.assertEqual(self.agent.agent_manage( "stop", "/data/omp_salt_agent")[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_agent_status_success(self, cmd, check): """ 测试查看状态成功 :return: """ self.assertEqual(self.agent.agent_manage( "status", "/data/omp_salt_agent")[0], True) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) def test_agent_status_failed(self, cmd, check): """ 测试查看状态失败 :return: """ self.assertEqual(self.agent.agent_manage( "status", "/data/omp_salt_agent")[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(True, "success")) def test_agent_restart_success(self, cmd, check): """ 测试重启成功 :return: """ self.assertEqual(self.agent.agent_manage( "restart", "/data/omp_salt_agent")[0], True) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) def test_agent_restart_failed(self, cmd, check): """ 测试重启失败 :return: """ self.assertEqual(self.agent.agent_manage( "restart", "/data/omp_salt_agent")[0], False) @mock.patch.object(SSH, "check", return_value=(True, "success")) @mock.patch.object(SSH, "cmd", return_value=(False, "success")) def test_agent_method_failed(self, cmd, check): """ 测试管理方法错误 :return: """ self.assertEqual(self.agent.agent_manage( "test", "/data/omp_salt_agent")[0], False) ================================================ FILE: omp_server/tests/test_utils/test_crontab_utils.py ================================================ # -*- coding: utf-8 -*- # Project: test_crontab_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-15 21:18 # IDE: PyCharm # Version: 1.0 # Introduction: """ 定时任务单元测试代码 """ import uuid from django_celery_beat.models import PeriodicTask from tests.base import BaseTest from utils.plugin.crontab_utils import CrontabUtils class CrontabUtilTest(BaseTest): def setUp(self): super(CrontabUtilTest, self).setUp() self.task_dic = { "task_name": str(uuid.uuid4()), "task_func": "test.test.func", "task_args": (1, ), "task_kwargs": {"test": "a"}, "task_timeout": None } def test_create_crontab_job(self): cron_obj = CrontabUtils(**self.task_dic) flag, msg = cron_obj.create_crontab_job() self.assertEqual(flag, True) is_exist = PeriodicTask.objects.filter( name=self.task_dic.get("task_name")).exists() self.assertEqual(is_exist, True) flag, msg = cron_obj.create_crontab_job() self.assertEqual(flag, False) flag, msg = cron_obj.delete_job() self.assertEqual(flag, True) flag, msg = cron_obj.delete_job() self.assertEqual(flag, False) def test_create_internal_job(self): cron_obj = CrontabUtils(**self.task_dic) flag, msg = cron_obj.create_internal_job(10) self.assertEqual(flag, True) is_exist = PeriodicTask.objects.filter( name=self.task_dic.get("task_name")).exists() self.assertEqual(is_exist, True) flag, msg = cron_obj.create_internal_job(10) self.assertEqual(flag, False) def test_create_internal_failed(self): cron_obj = CrontabUtils(**self.task_dic) flag, msg = cron_obj.create_internal_job("test") self.assertEqual(flag, False) flag, msg = cron_obj.create_internal_job(10, "test") self.assertEqual(flag, False) ================================================ FILE: omp_server/tests/test_utils/test_crypto.py ================================================ # -*- coding: utf-8 -*- # Project: test_crypto # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 20:00 # IDE: PyCharm # Version: 1.0 # Introduction: from tests.base import BaseTest from utils.plugin.crypto import AESCryptor class CryptoUtilTest(BaseTest): """ 加密解密测试类 """ def setUp(self): super(CryptoUtilTest, self).setUp() self.aes_obj = AESCryptor() self.test_str = "testStrings" self.encode_str = "uea_xeU_d_6YHCCY7Q-e2xZolSw2z2C3KGhLY6iMdnI" def test_encode_success(self): """ 测试加密 :return: """ self.assertEqual(self.encode_str, self.aes_obj.encode(self.test_str)) def test_decode_success(self): """ 测试解密 :return: """ self.assertEqual(self.test_str, self.aes_obj.decode(self.encode_str)) ================================================ FILE: omp_server/tests/test_utils/test_monitor_agent.py ================================================ # -*- coding: utf-8 -*- # Project: test_monitor_agent # Author: jon.liu@yunzhihui.com # Create time: 2021-10-07 15:47 # IDE: PyCharm # Version: 1.0 # Introduction: """ 测试monitor agent脚本 """ import os from unittest import mock from tests.base import BaseTest from db_models.models import Env from db_models.models import Host from omp_server.settings import PROJECT_DIR from utils.plugin.salt_client import SaltClient from promemonitor.prometheus_utils import PrometheusUtils from utils.plugin.monitor_agent import MonitorAgentManager class MonitorAgentTest(BaseTest): """ 主机Agent的测试类 """ def setUp(self): super(MonitorAgentTest, self).setUp() env_obj = Env() env_obj.name = "default" env_obj.save() self.correct_host_data = { "instance_name": "mysql_instance_1", "ip": "127.0.0.10", "port": 36000, "username": "root", "password": "root_password", "data_folder": "/data", "operate_system": "CentOS", "disk": {"/": 90, "/data": 99}, "env": env_obj } Host(**self.correct_host_data).save() self.host_obj_lst = list(Host.objects.all()) self.agent_path = os.path.join( PROJECT_DIR, "package_hub/omp_monitor_agent.tar.gz") if not os.path.exists(self.agent_path): with open(self.agent_path, "w") as fp: fp.write("test") self.monitor_agent_name = "omp_monitor_agent.tar.gz" def _install(self): manager = MonitorAgentManager(host_objs=self.host_obj_lst) manager.monitor_agent_package_name = self.monitor_agent_name return manager.install() @mock.patch.object(SaltClient, "cp_file", return_value=(True, "success")) @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_package_do_not_exist(self, *args, **kwargs): """ 测试monitor agent安装包不存在场景 :param args: :param kwargs: :return: """ manager = MonitorAgentManager(host_objs=self.host_obj_lst) manager.monitor_agent_package_name = "" flag, msg = manager.install() self.assertEqual(flag, False) @mock.patch.object(SaltClient, "cp_file", return_value=(False, "success")) @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_install_failed_send_package(self, *args, **kwargs): """ 测试成功安装monitor agent场景 :param args: :param kwargs: :return: """ flag, msg = self._install() self.assertEqual(flag, False) @mock.patch.object(SaltClient, "cp_file", return_value=(True, "success")) @mock.patch.object(SaltClient, "cmd", return_value=(False, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_install_failed_cmd(self, *args, **kwargs): """ 测试成功安装monitor agent场景 :param args: :param kwargs: :return: """ self.assertEqual(self._install()[0], False) @mock.patch.object(SaltClient, "cp_file", return_value=(True, "success")) @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) @mock.patch.object( PrometheusUtils, "add_node", return_value=(True, "success")) def test_install_success(self, *args, **kwargs): """ 测试成功安装monitor agent场景 :param args: :param kwargs: :return: """ flag, msg = self._install() self.assertEqual(flag, True) @mock.patch.object(SaltClient, "cmd", return_value=(False, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_uninstall_failed(self, *args, **kwargs): """ 测试卸载失败场景 :param args: :param kwargs: :return: """ manager = MonitorAgentManager(host_objs=self.host_obj_lst) manager.monitor_agent_package_name = self.monitor_agent_name self.assertEqual(manager.uninstall()[0], False) @mock.patch.object(SaltClient, "cmd", return_value=(True, "success")) @mock.patch.object(SaltClient, "__init__", return_value=None) def test_uninstall_success(self, *args, **kwargs): """ 测试卸载失败场景 :param args: :param kwargs: :return: """ manager = MonitorAgentManager(host_objs=self.host_obj_lst) manager.monitor_agent_package_name = self.monitor_agent_name self.assertEqual(manager.uninstall()[0], True) ================================================ FILE: omp_server/tests/test_utils/test_public_utils.py ================================================ # -*- coding: utf-8 -*- # Project: test_public_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-09 21:24 # IDE: PyCharm # Version: 1.0 # Introduction: """ 测试公共工具类 """ import os import uuid import socket from unittest import mock from tests.base import BaseTest from utils.plugin.public_utils import get_file_md5 from utils.plugin.public_utils import check_ip_port class GetFileMd5Test(BaseTest): """ 获取文件md5值测试 """ def setUp(self): super(GetFileMd5Test, self).setUp() self.file_path = os.path.realpath(__file__) def test_get_md5_failed_file_not_exist(self): """ 测试文件不存在时现象 :return: """ file_path = self.file_path + str(uuid.uuid4()) flag, message = get_file_md5(file_path) self.assertEqual(flag, False) def test_get_md5_success(self): """ 测试正常解析时现象 :return: """ flag, message = get_file_md5(self.file_path) self.assertEqual(flag, True) class CheckIpPortTest(BaseTest): """ ip 端口检查测试 """ def setUp(self): super(CheckIpPortTest, self).setUp() @mock.patch.object(socket.socket, "connect_ex", return_value=0) def test_success(self, *args, **kwargs): flag, msg = check_ip_port(ip="127.0.0.1", port=123) self.assertEqual(flag, True) @mock.patch.object(socket.socket, "connect_ex", return_value=1) def test_failed(self, *args, **kwargs): flag, msg = check_ip_port(ip="127.0.0.1", port=123) self.assertEqual(flag, False) def test_failed_with_ip_wrong(self): flag, msg = check_ip_port(ip="127", port=123) self.assertEqual(flag, False) self.assertEqual(msg, "ip address not correct") def test_failed_with_port_wrong(self): flag, msg = check_ip_port(ip="127.0.0.1", port=123456) self.assertEqual(flag, False) self.assertEqual(msg, "port must be 0 ~ 65535") def test_failed_with_port_str(self): flag, msg = check_ip_port(ip="127.0.0.1", port="port") self.assertEqual(flag, False) self.assertEqual(msg, "port must be 0 ~ 65535, int or string") ================================================ FILE: omp_server/tests/test_utils/test_salt_client.py ================================================ # -*- coding: utf-8 -*- # Project: test_salt_client # Author: jon.liu@yunzhihui.com # Create time: 2021-09-24 15:10 # IDE: PyCharm # Version: 1.0 # Introduction: """ salt client相关测试 """ from unittest import mock import salt.client from tests.base import BaseTest from utils.plugin.salt_client import SaltClient class SaltClientUtilTest(BaseTest): """ salt客户端测试类 """ @mock.patch.object(salt.client.LocalClient, "__init__", return_value=None) def setUp(self, local_client): super(SaltClientUtilTest, self).setUp() self.cmd_run = "cmd.run" self.obj = SaltClient() @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_salt_module_update_failed(self, local_client): """ 测试同步salt模块成功的情况 :return: """ self.assertEqual(self.obj.salt_module_update()[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={ '192.168.175': {'ret': [], 'retcode': 0, 'jid': '20210113213356939481'}, '192.168.176': False, '192.168.177': [], }) def test_salt_module_update_success(self, local_client): """ 测试同步salt模块成功的情况 :return: """ self.assertEqual(self.obj.salt_module_update()[0], True) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_fun_for_multi_failed(self, local_client): """ 测试批量执行时错误的情况 :return: """ local_client.side_effect = Exception("aa") self.assertEqual(self.obj.fun_for_multi("*", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={}) def test_fun_for_multi_success(self, local_client): """ 测试批量执行成功的情况 :return: """ self.assertEqual(self.obj.fun_for_multi("*", self.cmd_run), {}) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_fun_failed(self, local_client): """ 测试执行fun出现异常情况 :return: """ local_client.side_effect = Exception("aa") self.assertEqual(self.obj.fun("*", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={ "key1": {'ret': "success", 'retcode': 0, 'jid': '20210113213356939481'} }) def test_fun_success(self, local_client): """ 测试批量执行成功的情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], True) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_fun_failed_1(self, local_client): """ 测试salt.cmd返回不是字典的情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={}) def test_fun_failed_2(self, local_client): """ 测试salt.cmd返回中不带salt-key的情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": False}) def test_fun_failed_3(self, local_client): """ 测试salt.cmd返回中salt-key 为False情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": {}}) def test_fun_failed_4(self, local_client): """ 测试salt.cmd返回中retcode不存在情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": {'retcode': 1}}) def test_fun_failed_5(self, local_client): """ 测试salt.cmd返回中retcode不为0情况 :return: """ self.assertEqual(self.obj.fun("key1", self.cmd_run)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_cmd_failed(self, local_client): """ 测试执行cmd出现异常情况 :return: """ local_client.side_effect = Exception("aa") self.assertEqual(self.obj.cmd("*", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={ "key1": {'ret': "success", 'retcode': 0, 'jid': '20210113213356939481'} }) def test_cmd_success(self, local_client): """ 测试cmd执行成功的情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], True) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_cmd_failed_1(self, local_client): """ 测试salt.cmd返回不是字典的情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={}) def test_cmd_failed_2(self, local_client): """ 测试salt.cmd返回中不带salt-key的情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": False}) def test_cmd_failed_3(self, local_client): """ 测试salt.cmd返回中salt-key 为False情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": {}}) def test_cmd_failed_4(self, local_client): """ 测试salt.cmd返回中retcode不存在情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": {'retcode': 1}}) def test_cmd_failed_5(self, local_client): """ 测试salt.cmd返回中retcode不为0情况 :return: """ self.assertEqual(self.obj.cmd("key1", self.cmd_run, 1)[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_cp_file_failed(self, local_client): """ 测试执行cp_file出现异常情况 :return: """ local_client.side_effect = Exception("aa") self.assertEqual(self.obj.cp_file("*", "", "")[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={ "key1": "a" }) def test_cp_file_success(self, local_client): """ 测试推送文件成功的情况 :return: """ self.assertEqual(self.obj.cp_file("key1", "a", "a")[0], True) @mock.patch.object(salt.client.LocalClient, "cmd", return_value="") def test_cp_file_failed_1(self, local_client): """ 测试cp_file返回不是字典的情况 :return: """ self.assertEqual(self.obj.cp_file("*", "", "")[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={}) def test_cp_file_failed_2(self, local_client): """ 测试cp_file返回中不带salt-key的情况 :return: """ self.assertEqual(self.obj.cp_file("*", "", "")[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": "PermissionError Permission denied"}) def test_cp_file_failed_3(self, local_client): """ 测试cp_file返回中权限错误情况 :return: """ self.assertEqual(self.obj.cp_file("*", "", "")[0], False) @mock.patch.object(salt.client.LocalClient, "cmd", return_value={"key1": {"ret": ""}}) def test_cp_file_failed_4(self, local_client): """ 测试cp_file返回值不准确情况 :return: """ self.assertEqual(self.obj.cp_file("*", "aa", "aa")[0], False) ================================================ FILE: omp_server/tests/test_utils/test_ssh.py ================================================ # -*- coding: utf-8 -*- # Project: test_ssh # Author: jon.liu@yunzhihui.com # Create time: 2021-09-23 15:48 # IDE: PyCharm # Version: 1.0 # Introduction: """ ssh 单元测试代码 # TODO 待完善,与环境隔离 """ from unittest import mock from scp import SCPClient from paramiko import SSHClient from tests.base import BaseTest from utils.plugin.ssh import SSH def get_ssh_obj(username="root"): """ 获取ssh对象 :return: """ return SSH( hostname="127.0.0.1", port=22, username=username, password="root", timeout=1 ) class ChannelMock(object): """ 模拟channel """ def recv_exit_status(self): """ 模拟方法 :return: """ class StdoutMock(object): """ 模拟输出 """ def __init__(self, content): self.content = content self.channel = ChannelMock() def readline(self): """ 读取数据 :return: """ return self.content def readlines(self): """ 读取数据 :return: """ if self.content: return [self.content] return [] class SshUtilTest(BaseTest): """ ssh工具测试类 """ def setUp(self): super(SshUtilTest, self).setUp() self.ssh = get_ssh_obj() def test_get_connection_failed(self): """ 测试ssh连接失败信息 :return: """ self.assertEqual(self.ssh._get_connection(), None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) def test_get_connection_success(self, *args, **kwargs): """ 测试ssh连接正常 :return: """ self.assertEqual(self.ssh._get_connection(), None) self.assertEqual(self.ssh.is_error, None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_is_sudo_success(self, exec_command, *args, **kwargs): """ 测试sudo :return: """ stdout = StdoutMock("success") exec_command.side_effect = [("", stdout, 0), ] self.assertEqual(self.ssh.is_sudo()[0], True) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_is_sudo_failed(self, exec_command, *args, **kwargs): """ 测试sudo :return: """ stdout = StdoutMock("failed") exec_command.side_effect = [("", stdout, 0), ] self.assertEqual(self.ssh.is_sudo()[0], False) def test_is_sudo_failed_connected(self): """ 测试ssh检查报错,连接信息报错 :return: """ self.assertEqual(self.ssh.is_sudo()[0], False) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_check_failed(self, exec_command, *args, **kwargs): """ 测试ssh检查报错 :return: """ stdout = mock.MagicMock() exec_command.side_effect = [("", stdout, 0), ] self.assertEqual(self.ssh.check()[0], False) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_check_success(self, exec_command, *args, **kwargs): """ 测试ssh检查成功 :return: """ stdout = StdoutMock("root") exec_command.side_effect = [("", stdout, 0), ] self.assertEqual(self.ssh.check()[0], True) def test_ssh_check_failed_connected(self): """ 测试ssh检查报错,连接信息报错 :return: """ self.assertEqual(self.ssh.check()[0], False) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_cmd_failed(self, exec_command, *args, **kwargs): """ 测试执行命令失败 :return: """ stderr = StdoutMock("failed") stdout = StdoutMock("test") exec_command.side_effect = [("", stdout, stderr), ] self.assertEqual(self.ssh.cmd("aaa", get_pty=False)[0], False) def test_ssh_cmd_failed_connected(self): """ 测试ssh检查报错,连接信息报错 :return: """ self.assertEqual(self.ssh.cmd("ip a")[0], False) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_cmd_success(self, exec_command, *args, **kwargs): """ 测试执行命令成功 :return: """ stderr = StdoutMock("") stdout = StdoutMock("root") exec_command.side_effect = [("", stdout, stderr), ] self.assertEqual(self.ssh.cmd("whoami")[0], True) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SSHClient, "close", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SCPClient, "close", return_value=None) def test_ssh_close_success(self, *args, **kwargs): """ 关闭连接成功 :return: """ self.ssh._get_connection() self.assertEqual(self.ssh.close(), None) @mock.patch.object(SSH, "make_remote_path_exist", return_value=None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SCPClient, "put", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_file_push_success(self, *args, **kwargs): """ 发送文件成功 :return: """ self.assertEqual(self.ssh.file_push(__file__, "/tmp")[0], True) @mock.patch.object(SSH, "close", return_value=None) @mock.patch.object(SSH, "make_remote_path_exist", return_value=None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) @mock.patch.object(SSHClient, "exec_command", return_value=("", "success", 0)) def test_ssh_file_push_failed_exception(self, *args, **kwargs): """ 发送文件成功 :return: """ self.assertEqual(self.ssh.file_push(__file__, "/tmp")[0], False) def test_ssh_file_push_failed_connected(self): """ 发送文件失败 :return: """ self.assertEqual(self.ssh.file_push(__file__, "/tmp")[0], False) @mock.patch.object(SSH, "cmd", return_value=None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) def test_make_remote_path_exist_root(self, *args, **kwargs): """ 测试root用户情况下的远程目录存在 :return: """ self.assertEqual(self.ssh.make_remote_path_exist("/tmp"), None) @mock.patch.object(SSH, "cmd", return_value=None) @mock.patch.object(SSHClient, "set_missing_host_key_policy", return_value=None) @mock.patch.object(SSHClient, "connect", return_value=None) @mock.patch.object(SSHClient, "get_transport", return_value=None) @mock.patch.object(SCPClient, "__init__", return_value=None) def test_make_remote_path_exist_not_root(self, *args, **kwargs): """ 测试root用户情况下的远程目录存在 :return: """ # ssh_obj = get_ssh_obj("aaa") # self.assertEqual(ssh_obj.make_remote_path_exist("/tmp"), None) pass ================================================ FILE: omp_server/tool/__init__.py ================================================ ================================================ FILE: omp_server/tool/admin.py ================================================ from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/tool/apps.py ================================================ from django.apps import AppConfig class ToolConfig(AppConfig): name = 'tool' ================================================ FILE: omp_server/tool/find_tools.py ================================================ import os import uuid import logging from concurrent.futures import ThreadPoolExecutor, as_completed, wait, \ ALL_COMPLETED from django.conf import settings from ruamel import yaml from app_store.new_install_utils import RedisDB from db_models.models import ToolInfo from utils.parse_config import THREAD_POOL_MAX_WORKERS from utils.plugin.public_utils import file_md5, local_cmd logger = logging.getLogger("server") base_tar_path = os.path.join(settings.PROJECT_DIR, "package_hub/tool") verify_tar_path = os.path.join(base_tar_path, "verify_tar") verified_folder_path = os.path.join(base_tar_path, "folder") verified_tar_path = os.path.join(base_tar_path, "tar") class ValidForm: def __init__(self, form_list): self.form_list = form_list def base_valid(self, form): for k in ["key", "name"]: if not form.get(k): raise Exception(f"args中部分缺少{k}!") if not isinstance(form.get(k), str): raise Exception(f"args中{k}参数格式不正确!") if "key" == "output": raise Exception(f"args中key不可为output!") if not form.get("required"): form["required"] = False else: form["required"] = True def valid_file(self, form): if form.get("default"): form.pop("default") def valid_select(self, form): options = form.get("options") if not options: raise Exception(f"args中单选缺少options参数!") if not isinstance(options, list): raise Exception(f"args中单选options参数类型不正确!") for option in options: if not isinstance(option, str): raise Exception(f"args中单选options选项类型只支持字符串!") if form.get("default") and form.get("default") not in options: raise Exception(f"args中单选default选项必须在options中!") def valid_input(self, form): if form.get("default") and not isinstance(form.get("default"), str): raise Exception(f"args中文本内容default只支持字符串") def __call__(self, *args, **kwargs): for form in self.form_list: form_type = form.get("type") if not hasattr(self, f"valid_{form_type}"): raise Exception(f"暂不支持{form_type}类型") self.base_valid(form) getattr(self, f"valid_{form_type}")(form) return self.form_list class ValidToolTar: key_default = { "labels": "management", "spec": {"target": "host"}, "args": [], "output": {"type": "terminal", "required": False}, "send_package": [] } key_required = {"name", "desc", "script_name", "script_type"} def __init__(self, tmp_package, tar_file): self.tmp_package = tmp_package self.tar_file = tar_file self.tool_info = {} def verify_args(self, script_args): ValidForm(script_args) self.tool_info["script_args"] = script_args return True verify_args._type = list def verify_name(self, name): if not bool(name): raise Exception("name不可为空!") self.tool_info["name"] = name return True verify_name._type = str def verify_desc(self, desc): if not bool(desc): raise Exception("desc不可为空!") self.tool_info["description"] = desc return True verify_desc._type = str def verify_labels(self, kind): if not hasattr(ToolInfo, f"KIND_{kind.upper()}"): raise Exception("yaml中labels类型不支持!") self.tool_info["kind"] = getattr(ToolInfo, f"KIND_{kind.upper()}") return True verify_labels._type = str def verify_spec(self, spec): target_name = spec.get("target", "host") if not isinstance(target_name, str): raise Exception("yaml中target参数类型错误!") self.tool_info["target_name"] = target_name templates = spec.get("templates") or [] if not isinstance(templates, list): raise Exception("templates参数类型不正确!") for template in templates: if not os.path.isfile(os.path.join(self.folder_path, template)): raise Exception(f"模版文件{template}文件不存在!") self.tool_info["template_filepath"] = templates connection_args = spec.get("connection_args") or [] if not isinstance(connection_args, list): raise Exception("connection_args参数类型不正确!") for connection_arg in connection_args: if not isinstance(connection_arg, str): raise Exception("connection_args参数类型不正确!") self.tool_info["obj_connection_args"] = connection_args return True verify_spec._type = dict def verify_output(self, output): if not hasattr(ToolInfo, f"OUTPUT_{output.get('type').upper()}"): raise Exception("output参数只能是terminal或file!") self.tool_info["output"] = output return True verify_output._type = dict def verify_send_package(self, send_package): for package in send_package: if not os.path.isfile(os.path.join(self.folder_path, package)): raise Exception(f"需要发送的文件{package}文件不存在!") self.tool_info["send_package"] = send_package return True verify_send_package._type = list def verify_script_name(self, script_name): if not os.path.isfile(os.path.join(self.folder_path, script_name)): raise Exception(f"需要发送的文件{script_name}文件不存在!") self.tool_info["script_path"] = script_name return True verify_script_name._type = str def verify_script_type(self, script_type): if not hasattr(ToolInfo, f"SCRIPT_TYPE_{script_type.upper()}"): raise Exception("script_type参数只能是python3或shell!") self.tool_info["script_type"] = getattr( ToolInfo, f"SCRIPT_TYPE_{script_type.upper()}" ) return True verify_script_type._type = str def verify_yaml_info(self, package_name): yaml_path = os.path.join(self.folder_path, f"{package_name}.yaml") with open(yaml_path, "r", encoding="utf8") as fp: content = yaml.load(fp.read(), yaml.Loader) for k in self.key_required: if k not in content: raise Exception(f"yaml中{k}参数为必填!") for k, default in self.key_default.items(): if not content.get(k): content[k] = default for k, v in content.items(): verify_func = getattr(self, f"verify_{k}") if not isinstance(v, getattr(verify_func, "_type")): raise Exception(f"{k}参数类型不正确!") verify_func(v) return True def read_read_me(self): read_me_path = os.path.join(self.folder_path, "README.md") with open(read_me_path, "r", encoding="utf-8") as f: data = f.read() self.tool_info["readme_info"] = data def create_tool_info(self, package_name, md5): tar_save_name = self.tar_file.replace(".tar.gz", f"-{md5}.tar.gz") new_tar_path = os.path.join(verified_tar_path, tar_save_name) old_tar_path = os.path.join(self.tmp_package, self.tar_file) old_package_folder = os.path.join( self.tmp_package, f"{package_name}_{md5}/{package_name}") new_package_folder = os.path.join( verified_folder_path, f"{package_name}-{md5}") _out, _err, _code = local_cmd( f"mv {old_tar_path} {new_tar_path} && " f"mv {old_package_folder} {new_package_folder}" ) if _code: return _out self.tool_info["source_package_md5"] = md5 tool_folder = f"tool/folder/{package_name}-{md5}" self.tool_info["tool_folder_path"] = tool_folder self.tool_info["source_package_path"] = f"tool/tar/{tar_save_name}" output_dict = self.tool_info.pop("output") output = getattr(ToolInfo, f"OUTPUT_{output_dict.get('type').upper()}") if output == ToolInfo.OUTPUT_FILE: self.tool_info["script_args"].append( { 'key': 'output', 'name': 'output', 'type': 'input', 'required': output_dict.get("required", False) } ) if os.path.exists(os.path.join(new_package_folder, 'logo.svg')): self.tool_info["logo"] = f"{tool_folder}/logo.svg" ToolInfo(output=output, **self.tool_info).save() self.rm_tool_package() def rm_tool_package(self): local_cmd(f"/bin/rm -rf {self.tmp_package}") def __call__(self, *args, **kwargs): package_name = self.tar_file.split("-")[0] if not package_name: return f"{self.tar_file}名称不符合要求!" file_path = os.path.join(self.tmp_package, self.tar_file) md5 = file_md5(file_path) if not md5: local_cmd(f'/bin/rm -rf {file_path}') return f"获取{self.tar_file}的md5值失败!" if ToolInfo.objects.filter(source_package_md5=md5).exists(): return f"{self.tar_file}已存在!" tar_folder = os.path.join(self.tmp_package, f"{package_name}_{md5}") _out, _err, _code = local_cmd( f"mkdir -p {tar_folder} && tar -mxf {file_path} -C {tar_folder}" ) if _code: self.rm_tool_package() return _out self.folder_path = os.path.join(tar_folder, package_name) if not os.path.isfile( os.path.join(self.folder_path, f"{package_name}.yaml") ): self.rm_tool_package() return f"{self.tar_file}中必须包含文件{package_name}.yaml!" try: self.verify_yaml_info(package_name) except Exception as e: self.rm_tool_package() return str(e) if os.path.isfile(os.path.join(self.folder_path, "README.md")): self.read_read_me() self.create_tool_info(package_name, md5) return "" def verify_tar_files(tmp_package, tar_files): with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: all_task = [] for tar_file in tar_files: valid_obj = ValidToolTar(tmp_package, tar_file) future_obj = executor.submit(valid_obj) all_task.append(future_obj) wait(all_task, return_when=ALL_COMPLETED) success = True for future in as_completed(all_task): if future.result(): logger.info(future.result()) success = False local_cmd(f"/bin/rm -rf {tmp_package}") return success def load_verify_tar(tar_name=None): tmp_package = os.path.join( settings.PROJECT_DIR, "tmp", uuid.uuid4().hex) local_cmd(f'mkdir -p {tmp_package}') tar_files = [] with RedisDB().conn.lock(settings.SCAN_TOOL_LOCK_KEY): if tar_name: file_path = os.path.join(verify_tar_path, tar_name) if os.path.exists(file_path): local_cmd(f'mv {file_path} {tmp_package}') tar_files.append(tar_name) return tmp_package, tar_files tar_packages = os.listdir(verify_tar_path) for tar_package in tar_packages: file_path = os.path.join(verify_tar_path, tar_package) if not tar_package.endswith(".tar.gz"): local_cmd(f'/bin/rm -rf {file_path}') continue local_cmd(f'mv {file_path} {tmp_package}') tar_files.append(tar_package) return tmp_package, tar_files def find_tools_package(tar_name=None): tmp_package, tar_files = load_verify_tar(tar_name) return verify_tar_files(tmp_package, tar_files) ================================================ FILE: omp_server/tool/serializers.py ================================================ import json import os import random import string from django.db import transaction from rest_framework import serializers from db_models.models import ToolExecuteMainHistory, ToolInfo, Host, Service, \ ToolExecuteDetailHistory, UploadFileHistory from tool.tasks import exec_tools_main from utils.common.exceptions import GeneralError class ToolInfoSerializer(serializers.ModelSerializer): class Meta: """ 元数据 """ model = ToolInfo fields = ("target_name",) class ToolDetailSerializer(serializers.ModelSerializer): tool = ToolInfoSerializer() tool_detail = serializers.SerializerMethodField() count = serializers.SerializerMethodField() tool_args = serializers.SerializerMethodField() duration = serializers.SerializerMethodField() run_user = serializers.SerializerMethodField() time_out = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = ToolExecuteMainHistory fields = ( "tool", "tool_detail", "count", "tool_args", "duration", "run_user", "time_out", "task_name", "operator", "status", "start_time", "end_time" ) def tools_boj_ls(self, obj): if hasattr(self, "tools_obj"): return self.tools_obj tools_obj = ToolExecuteDetailHistory.objects.filter(main_history=obj) setattr(self, "tools_obj", tools_obj) return tools_obj def get_duration(self, obj): return obj.duration def get_run_user(self, obj): user = self.tools_boj_ls(obj).first().run_user user = user if user else "salt执行用户" return user def get_time_out(self, obj): return self.tools_boj_ls(obj).first().time_out def get_count(self, obj): return self.tools_boj_ls(obj).count() def get_tool_detail(self, obj): """ 获取detail详情 """ tool_list = [] for obj in self.tools_boj_ls(obj): url = "" if obj.output: url = f"tool/download_data/{obj.output.get('file')[0]}" tool_list.append( { "ip": obj.target_ip, "status": obj.status, "log": obj.execute_log, "url": url } ) return tool_list def get_tool_args(self, obj): tool_args = [] detail_args = obj.toolexecutedetailhistory_set.first().execute_args for args in obj.tool.script_args: value = detail_args.get(args.get('key'), "") if value: tool_args.append({ "name": args.get('name'), "value": value }) return tool_args class ToolFormDetailSerializer(serializers.ModelSerializer): default_form = serializers.DictField(source="load_default_form") class Meta: """ 元数据 """ model = ToolInfo fields = ("id", "name", "default_form", "script_args") class ToolListSerializer(serializers.ModelSerializer): """工具列表序列化""" used_number = serializers.SerializerMethodField() def get_used_number(self, obj): return ToolExecuteMainHistory.objects.filter(tool=obj).count() class Meta: model = ToolInfo fields = ("id", "logo", "name", "kind", "used_number", "description") class ToolInfoDetailSerializer(serializers.ModelSerializer): """工具详情序列化""" class Meta: model = ToolInfo fields = ("name", "description", "logo", "tar_url", "kind", "target_name", "script_path", "script_args", "templates", "readme_info") class ToolTargetObjectHostSerializer(serializers.ModelSerializer): host_agent_state = serializers.SerializerMethodField() def get_host_agent_state(self, obj): if obj.host_agent == str(obj.AGENT_RUNNING): return "正常" return "异常" class Meta: """ 元数据 """ model = Host fields = ("id", "instance_name", "ip", "host_agent_state") class ToolTargetObjectServiceSerializer(serializers.ModelSerializer): instance_name = serializers.CharField(source="service_instance_name") host_agent_state = serializers.SerializerMethodField() modifiable_kwargs = serializers.SerializerMethodField() def get_host_agent_state(self, obj): host = Host.objects.filter(ip=obj.ip).first() if host and host.host_agent == str(host.AGENT_RUNNING): return "正常" return "异常" def get_modifiable_kwargs(self, obj): modifiable_kwargs = {} tool = self.context.get("view").kwargs["tool"] connection_args = tool.obj_connection_args connect_obj = obj.service_connect_info port_infos = json.loads(obj.service_port) port_dict = {} for port_info in port_infos: port_dict.update({port_info.get("key"): port_info.get("default")}) for arg_key in connection_args: if arg_key in port_dict: modifiable_kwargs[arg_key] = port_dict[arg_key] elif connect_obj and hasattr(connect_obj, arg_key): modifiable_kwargs[arg_key] = getattr(connect_obj, arg_key) else: modifiable_kwargs[arg_key] = "" return modifiable_kwargs class Meta: """ 元数据 """ model = Service fields = ("id", "instance_name", "ip", "host_agent_state", "modifiable_kwargs") class ValidFormAnswer: def __init__(self, questions, answers): self.questions = questions self.answers = answers def valid_file(self, question): if not question.get("default", {}): return True file_union_id = question.get("default", {}).get("union_id") file = UploadFileHistory.objects.filter(union_id=file_union_id).last() if not file: raise GeneralError(f"表单{question.get('name')}提交的文件不存在!") question["default"].update( file_name=file.file_name, file_url=file.file_url ) return True def valid_select(self, question): options = question.get("options") answer = question.get("default") if answer and answer not in options: raise GeneralError(f"表单{question.get('name')}提交的选项不正确!") return True def valid_input(self, question): return True def is_valid(self): for question in self.questions: form_type = question.get("type") if not hasattr(self, f"valid_{form_type}"): raise GeneralError(f"暂不支持{form_type}类型") answer = self.answers.get(question.get("key")) if question.get("required") and not answer: raise GeneralError(f"{question.get('name')}为必填!") question.update(default=answer) getattr(self, f"valid_{form_type}")(question) return self.questions class ToolFormAnswerSerializer(serializers.Serializer): id = serializers.IntegerField(read_only=True, default=0) default_form = serializers.DictField( help_text="默认表单", required=True, error_messages={"required": "默认表单为必填"} ) script_args = serializers.ListField(help_text="自定义参数", required=False) def verify_task_name(self, value): if not value: raise GeneralError("任务名称为必填字段") return str(value) def verify_host_info(self, values): target_ips = Host.objects.filter( id__in=[value.get("id") for value in values], host_agent=str(Host.AGENT_RUNNING) ).values("ip", "data_folder") if len(target_ips) != len(values): raise GeneralError("主机数据异常,请重新选择执行对象!") data_folders = {} for target_ip in target_ips: data_folders[target_ip["ip"]] = target_ip["data_folder"] self.context.get("view").kwargs["data_folders"] = data_folders def verify_service_info(self, values, tool): ids = [] for value in values: ids.append(value.get("id")) modifiable_kwargs = value.get("modifiable_kwargs", {}) for _arg in tool.obj_connection_args: if _arg not in modifiable_kwargs: raise GeneralError(f"参数{_arg}必填!") target_ips = list( Service.objects.filter( service__app_name=tool.target_name, id__in=ids ).values_list("ip", flat=True) ) if len(target_ips) != len(values): raise GeneralError("服务数据异常,请重新选择执行对象!") ips = set(target_ips) hosts = Host.objects.filter(ip__in=ips).values("ip", "data_folder") data_folders = {} for host in hosts: data_folders[host["ip"]] = host["data_folder"] self.context.get("view").kwargs["data_folders"] = data_folders def verify_target_objs(self, values): tool = self.context.get("view").kwargs["tool"] if tool.target_name == "host": self.verify_host_info(values) else: self.verify_service_info(values, tool) return True def verify_runuser(self, value): return True def verify_timeout(self, value): if not value: raise GeneralError("超时时间不可以等于0!") return True def validate_default_form(self, value): tool = self.context.get("view").kwargs["tool"] for k in tool.load_default_form().keys(): getattr(self, f"verify_{k}")(value.get(k)) return value def validate_script_args(self, value): tool = self.context.get("view").kwargs["tool"] if not tool.script_args: return [] answers = {} for script_arg in value: answers[script_arg.get("key")] = script_arg.get("default") form_answers = ValidFormAnswer(tool.script_args, answers).is_valid() return form_answers @transaction.atomic def create(self, validated_data): view_kwargs = self.context.get("view").kwargs tool = view_kwargs["tool"] request = self.context.get("request") default_form = validated_data.get("default_form") script_args = validated_data.get("script_args", []) history = ToolExecuteMainHistory.objects.create( tool=tool, task_name=default_form.get("task_name"), operator=request.user.username, form_answer=validated_data ) common_args = {} file_args = {} for script_arg in script_args: if script_arg.get("type") == "file": file_name = script_arg.get("default", {}).get("file_name") if not file_name: continue file_args[script_arg.get("key")] = script_arg.get( "default", {}).get("file_url") else: common_args[script_arg.get("key")] = script_arg.get("default") execute_details = [] for target_obj in default_form.get("target_objs"): target_detail = { "target_ip": target_obj.get("ip"), "main_history": history, "time_out": default_form.get("timeout"), "run_user": default_form.get("runuser") } execute_args = { "ip": target_obj.get("ip"), **target_obj.get("modifiable_kwargs", {}), **common_args } remote_folder = os.path.join( view_kwargs["data_folders"].get(target_obj.get("ip"), "/tmp"), "omp_packages" ) # output file if "output" in execute_args: file_name = execute_args.get('output') if file_name: random_str = ''.join( random.sample(string.digits+string.ascii_lowercase, 6)) file_name = f"{random_str}-{file_name}" execute_args["output"] = os.path.join( remote_folder, tool.tool_folder_path, f"{file_name}" ) target_detail["output"] = {"file": [file_name]} if not file_name: execute_args.pop('output') # input file for k, file_url in file_args.items(): execute_args[k] = os.path.join(remote_folder, file_url) execute_details.append( ToolExecuteDetailHistory( **target_detail, execute_args=execute_args, ) ) ToolExecuteDetailHistory.objects.bulk_create(execute_details) exec_tools_main.delay(history.id) validated_data.update(id=history.id) return validated_data class ToolExecuteHistoryListSerializer(serializers.ModelSerializer): kind = serializers.CharField(source="tool.kind") class Meta: model = ToolExecuteMainHistory fields = ("id", "tool_id", "task_name", "kind", "start_time", "status", "duration") ================================================ FILE: omp_server/tool/tasks.py ================================================ """ 服务相关异步任务 """ import logging import os import time from concurrent.futures import ThreadPoolExecutor, as_completed, wait, \ ALL_COMPLETED from django.utils import timezone from celery import shared_task from celery.utils.log import get_task_logger from db_models.models import ToolExecuteMainHistory, ToolExecuteDetailHistory from utils.plugin.salt_client import SaltClient from utils.plugin import public_utils THREAD_POOL_MAX_WORKERS = 20 # 屏蔽celery任务日志中的paramiko日志 logging.getLogger("paramiko").setLevel(logging.WARNING) logger = get_task_logger("celery_log") class ThreadUtils: def __init__(self): self.timeout = 10 self.salt = SaltClient() self.salt_data = self.salt.client.opts.get("root_dir") self.count = 0 @staticmethod def send_message(tool_detail_obj, index=None, message=None): """ 标准打印日志 """ message_info = ["占位", "开始执行工具包", "开始获取输出文件", "工具执行成功", "开始发送工具包"] if index: message = message_info[index] tool_detail_obj.execute_log += "{1} {0}\n".format( message, timezone.now()) tool_detail_obj.save() def receive_file(self, tool_detail_obj, receive_files, ip): """ 接收文件 """ self.send_message(tool_detail_obj, 2) pull_dc = receive_files.get("output_files", []) receive_to = receive_files.get("receive_to", "/tmp") upload_real_paths = [] for file in pull_dc: status, message = self.salt.cp_push( target=ip, source_path=file, upload_path=file.rsplit("/", 1)[1]) upload_real_paths.append( os.path.join(self.salt_data, f"var/cache/salt/master/minions/{ip}/files/{file.rsplit('/', 1)[1]}")) if not status: tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_FAILED self.send_message(tool_detail_obj, message=message) return False if upload_real_paths: _out, _err, _code = public_utils.local_cmd( f'mv {" ".join(upload_real_paths)} {receive_to}') if _code != 0: tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_FAILED self.send_message(tool_detail_obj, message=_out) return False return True def __call__(self, tool_detail_obj, *args, **kwargs): """ 执行单个工具任务函数 """ tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_RUNNING # 发送文件 ip = tool_detail_obj.target_ip self.send_message(tool_detail_obj, 4) send_dc = tool_detail_obj.get_send_files() for file in send_dc: status, message = self.salt.cp_file( target=ip, source_path=file.get("local_file"), target_path=file.get("remote_file") ) if not status: tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_FAILED self.send_message(tool_detail_obj, message=message) return status, message # 执行脚本 self.send_message(tool_detail_obj, 1) cmd_str = tool_detail_obj.get_cmd_str() if tool_detail_obj.run_user: cmd_str = 'su -s /bin/bash {1} -c "{0}"'.format( cmd_str, tool_detail_obj.run_user ) self.send_message(tool_detail_obj, message=f"执行脚本的命令: {cmd_str}") status, message = self.salt.cmd( target=ip, command=cmd_str, timeout=self.timeout, real_timeout=tool_detail_obj.time_out ) if not status: if 'Timed out' in message: tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_TIMEOUT else: tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_FAILED self.send_message(tool_detail_obj, message=message) return status, message self.send_message(tool_detail_obj, message=f"脚本输出如下: {message}") # 获取目标输出文件 receive_files = tool_detail_obj.get_receive_files() if receive_files: status = self.receive_file(tool_detail_obj, receive_files, ip) if not status: return False, "执行失败" self.send_message(tool_detail_obj, 3) tool_detail_obj.status = ToolExecuteDetailHistory.STATUS_SUCCESS tool_detail_obj.save() return True, "执行成功" @shared_task def exec_tools_main(tool_main_id): """ 工具执行类 """ # 当磁盘写入较慢时需稍等 time.sleep(2) tool_main_obj = ToolExecuteMainHistory.objects.select_related().filter(id=tool_main_id) exec_ing_dc = { "status": ToolExecuteMainHistory.STATUS_RUNNING, "start_time": timezone.now() } if tool_main_obj.exists(): tool_main_obj.update(**exec_ing_dc) else: logger.error(f"主工具执行id不存在{tool_main_id}") raise ValueError(f"主工具执行id不存在{tool_main_id}") # 开始下发各个目标节点任务 tool_detail_objs = tool_main_obj.first().toolexecutedetailhistory_set.all() # tool_detail_objs = ToolExecuteMainHistory.objects.filter( # tool=tool_main_obj) with ThreadPoolExecutor(THREAD_POOL_MAX_WORKERS) as executor: future_list = [] for obj in tool_detail_objs: future_obj = executor.submit(ThreadUtils(), obj) future_list.append(future_obj) wait(future_list, return_when=ALL_COMPLETED) success = True for future in as_completed(future_list): if not future.result(): success = False break # 查看各个任务执行状态,修改主状态页。 exec_ed_status = ToolExecuteMainHistory.STATUS_FAILED if not success \ else ToolExecuteMainHistory.STATUS_SUCCESS exec_ed_dc = { "status": exec_ed_status, "end_time": timezone.now() } tool_main_obj.update(**exec_ed_dc) ================================================ FILE: omp_server/tool/tests.py ================================================ from django.test import TestCase # Create your tests here. ================================================ FILE: omp_server/tool/tool_filters.py ================================================ # -*- coding:utf-8 -*- # Project: tool_filters # Create time: 2022/2/10 3:23 下午 import django_filters from django_filters.rest_framework import FilterSet from rest_framework.filters import BaseFilterBackend from db_models.models import ToolInfo class ToolFilter(FilterSet): name = django_filters.CharFilter( help_text="实用工具名称", field_name="name", lookup_expr="icontains") kind = django_filters.NumberFilter( help_text="实用工具分类:0-管理工具;2-安全工具", field_name="kind", lookup_expr="exact" ) class Meta: model = ToolInfo fields = ("name", "kind") class ToolInfoKindFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): param = request.query_params.get("kind", "") param = param.replace('\x00', '').replace('null', '') if not param: return queryset queryset = queryset.filter(tool__kind=int(param)) return queryset ================================================ FILE: omp_server/tool/urls.py ================================================ # -*- coding:utf-8 -*- # Project: urls # Create time: 2022/2/10 6:23 下午 from django.urls import path from rest_framework.routers import DefaultRouter from tool.views import ToolListView, ToolDetailView, GetToolDetailView, \ ToolFormDetailAPIView, ToolTargetObjectAPIView, ToolFormAnswerAPIView, \ ToolExecuteHistoryListApiView router = DefaultRouter() router.register("toolList", ToolListView, basename="toolList") router.register("toolList", ToolDetailView, basename="toolList") router.register(r'result', GetToolDetailView, basename="result") router.register(r'form', ToolFormDetailAPIView, basename="form") urlpatterns = [ path( 'form//target-object', ToolTargetObjectAPIView.as_view(), name="target-object" ), path( 'form//answer', ToolFormAnswerAPIView.as_view(), name="answer" ), path( 'execute-history', ToolExecuteHistoryListApiView.as_view(), name="execute-history" ), ] urlpatterns += router.urls ================================================ FILE: omp_server/tool/views.py ================================================ # Create your views here. from django.shortcuts import get_object_or_404 from django_filters.rest_framework.backends import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter from rest_framework.generics import ListAPIView, CreateAPIView from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import RetrieveModelMixin, ListModelMixin from db_models.models import (ToolExecuteMainHistory, ToolInfo, Host, Service) from tool.tool_filters import ToolFilter, ToolInfoKindFilter from tool.serializers import ToolListSerializer, ToolInfoDetailSerializer, \ ToolTargetObjectServiceSerializer, ToolFormAnswerSerializer, \ ToolExecuteHistoryListSerializer from utils.common.paginations import PageNumberPager from tool.serializers import ToolDetailSerializer, ToolFormDetailSerializer, \ ToolTargetObjectHostSerializer class ToolRetrieveAPIMixin: def load_tool_obj(self): lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field tool = get_object_or_404( ToolInfo.objects.all(), **{self.lookup_field: self.kwargs[lookup_url_kwarg]} ) self.kwargs.update(tool=tool) return tool class GetToolDetailView(GenericViewSet, RetrieveModelMixin): """ 任务详情页 """ queryset = ToolExecuteMainHistory.objects.all() get_description = "任务详情页" serializer_class = ToolDetailSerializer class ToolFormDetailAPIView(GenericViewSet, RetrieveModelMixin): queryset = ToolInfo.objects.all() get_description = "小工具执行表单页" serializer_class = ToolFormDetailSerializer class ToolListView(GenericViewSet, ListModelMixin): """查询所有实用工具列表""" queryset = ToolInfo.objects.all().order_by("-created") serializer_class = ToolListSerializer pagination_class = PageNumberPager # 过滤排序字段 filter_backends = (DjangoFilterBackend,) filter_class = ToolFilter # 操作信息描述 get_description = "查询所有实用工具列表" class ToolDetailView(GenericViewSet, RetrieveModelMixin): """获取实用工具详情""" queryset = ToolInfo.objects.all().order_by("-created") serializer_class = ToolInfoDetailSerializer # 操作描述信息 get_description = "获取实用工具详情" class ToolTargetObjectAPIView(ListAPIView, ToolRetrieveAPIMixin): get_description = "小工具执行对象展示页" pagination_class = PageNumberPager def get(self, request, *args, **kwargs): self.load_tool_obj() return self.list(request, *args, **kwargs) def get_queryset(self): if self.kwargs["tool"].target_name == "host": return Host.objects.all() return Service.objects.filter( service__app_name=self.kwargs["tool"].target_name ) def get_serializer_class(self): if self.kwargs["tool"].target_name == "host": return ToolTargetObjectHostSerializer return ToolTargetObjectServiceSerializer class ToolFormAnswerAPIView(CreateAPIView, ToolRetrieveAPIMixin): get_description = "小工具执行表单页" serializer_class = ToolFormAnswerSerializer def post(self, request, *args, **kwargs): self.load_tool_obj() return self.create(request, *args, **kwargs) class ToolExecuteHistoryListApiView(ListAPIView): get_description = "小工具执行列表页" pagination_class = PageNumberPager serializer_class = ToolExecuteHistoryListSerializer queryset = ToolExecuteMainHistory.objects.all().select_related("tool") filter_backends = (SearchFilter, OrderingFilter, ToolInfoKindFilter) search_fields = ("task_name", ) ordering_fields = ("start_time",) ordering = ('-start_time',) ================================================ FILE: omp_server/users/__init__.py ================================================ ================================================ FILE: omp_server/users/admin.py ================================================ # from django.contrib import admin # Register your models here. ================================================ FILE: omp_server/users/apps.py ================================================ from django.apps import AppConfig class UsersConfig(AppConfig): name = 'users' ================================================ FILE: omp_server/users/urls.py ================================================ # -*- coding: utf-8 -*- # Project: urls # Author: jon.liu@yunzhihui.com # Create time: 2021-09-10 17:21 # IDE: PyCharm # Version: 1.0 # Introduction: """ 用户相关的路由 """ from rest_framework.routers import DefaultRouter from users.views import ( UsersView, OperateLogView, UserUpdatePasswordView, UserLoginOperateView, CaptchaView ) router = DefaultRouter() router.register("users", UsersView, basename="users") router.register("operateLog", OperateLogView, basename="operateLog") router.register("UserLoginLog", UserLoginOperateView, basename="operateLog") router.register("updatePassword", UserUpdatePasswordView, basename="updatePassword") router.register("captcha", CaptchaView, basename="captcha") ================================================ FILE: omp_server/users/users_filters.py ================================================ """ 用户相关过滤器 """ import django_filters from django_filters.rest_framework import FilterSet from db_models.models import ( UserProfile, OperateLog, UserLoginLog ) class UserFilter(FilterSet): """ 主机过滤类 """ username = django_filters.CharFilter( help_text="用户名,模糊匹配", field_name="username", lookup_expr="icontains") class Meta: model = UserProfile fields = ("username",) class UserOperateFilter(FilterSet): """ 用户操作过滤类 """ username = django_filters.CharFilter( help_text="操作用户", field_name="username", lookup_expr="icontains") class Meta: model = OperateLog fields = ("username",) class UserLoginOperateFilter(FilterSet): """ 用户登陆日志过滤类 """ username = django_filters.CharFilter( help_text="操作用户", field_name="username", lookup_expr="icontains") class Meta: model = UserLoginLog fields = ("username",) ================================================ FILE: omp_server/users/users_serializers.py ================================================ # -*- coding: utf-8 -*- # Project: users_serializers # Author: jon.liu@yunzhihui.com # Create time: 2021-09-10 17:16 # IDE: PyCharm # Version: 1.0 # Introduction: """ 用户序列化使用方法 """ from django.contrib.auth import authenticate from django.contrib.auth.hashers import make_password from rest_framework import serializers from rest_framework.exceptions import ValidationError from rest_framework.serializers import ( ModelSerializer, Serializer ) from rest_framework_jwt.serializers import JSONWebTokenSerializer from db_models.models import ( UserProfile, OperateLog, UserLoginLog ) from utils.common.validators import UserPasswordValidator from utils.plugin.crypto import decrypt_rsa class UserSerializer(ModelSerializer): """ 用户序列化类 """ re_password = serializers.CharField( max_length=32, required=True, write_only=True, error_messages={"required": "必须包含re_password字段"}, help_text="二次确认密码") email = serializers.EmailField( required=True, error_messages={"required": "必须包含email字段", "invalid": "邮箱格式不正确"}, help_text="电子邮件") password = serializers.CharField( max_length=32, required=True, write_only=True, error_messages={"required": "必须包含password字段"}, help_text="密码") username = serializers.CharField( max_length=32, required=True, error_messages={"required": "必须包含名字"}, help_text="用户名") class Meta: """ 元数据 """ model = UserProfile fields = ("id", "username", "password", "email", "re_password", "date_joined", "is_active", "is_superuser", "role") read_only_fields = ("date_joined", "is_active", "is_superuser") def validate_username(self, username): """ 校验用户名是否唯一 :param username: 用户名 :return: """ request = self.context["request"] if request.method != "PUT" and \ UserProfile.objects.filter(username=username).count() != 0: raise ValidationError("用户名已存在") return username def validate(self, attrs): """ 校验 :param attrs: :return: """ password = attrs.get('password') re_password = attrs.pop('re_password') if password != re_password: raise ValidationError({'re_password': '两次密码不一致'}) attrs["password"] = make_password(password) return attrs class OperateLogSerializer(ModelSerializer): """ 用户操作记录序列化 """ create_time = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = OperateLog fields = ( "id", "username", "request_ip", "request_method", "description", "create_time" ) def get_create_time(self, obj): if obj.create_time: return str(obj.create_time).split(".")[0] return obj.create_time class UserLoginOperateSerializer(ModelSerializer): """登录记录序列化""" login_time = serializers.SerializerMethodField() class Meta: """ 元数据 """ model = UserLoginLog fields = "__all__" def get_login_time(self, obj): if obj.login_time: return str(obj.login_time).split(".")[0] return obj.login_time class JwtSerializer(JSONWebTokenSerializer): """ Jwt序列化类 """ remember = serializers.BooleanField( required=False, default=False, help_text="Boolean类型,缺省值为False") def validate(self, attrs): validate_dict = super(JwtSerializer, self).validate(attrs) validate_dict["remember"] = attrs.get("remember") return validate_dict class UserUpdatePasswordSerializer(Serializer): """ 用户更新密码序列化器 """ username = serializers.CharField( help_text="用户名", min_length=344, max_length=344, required=True, error_messages={"required": "必须包含名字"}) old_password = serializers.CharField( help_text="原密码", required=True, min_length=344, max_length=344, error_messages={"required": "必须包含password字段"} ) new_password = serializers.CharField( help_text="新密码", required=True, min_length=344, max_length=344, error_messages={"required": "必须包含new_password字段"} ) def validate(self, attrs): """ 校验,用户的原密码是否正确 """ credentials = { "username": decrypt_rsa(attrs.get("username")), "password": decrypt_rsa(attrs.get("old_password")) } user = authenticate(**credentials) if not user: raise ValidationError({"old_password": "当前密码不正确"}) new_password = decrypt_rsa(attrs.get("new_password")) UserPasswordValidator()(new_password, self.fields.get("new_password")) attrs["user_obj"] = user attrs["new_password"] = new_password return attrs def create(self, validated_data): """ 用户密码加密入库 """ user = validated_data["user_obj"] user.set_password(validated_data.get("new_password")) user.save() return validated_data ================================================ FILE: omp_server/users/views.py ================================================ """ 用户视图相关函数 """ import re import datetime from io import BytesIO from django.http import HttpResponse from rest_framework.exceptions import ValidationError from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ( ListModelMixin, CreateModelMixin, RetrieveModelMixin, DestroyModelMixin, UpdateModelMixin ) from rest_framework_jwt.views import JSONWebTokenAPIView from rest_framework_jwt.settings import api_settings from django_filters.rest_framework.backends import DjangoFilterBackend from db_models.models import ( UserProfile, OperateLog, UserLoginLog ) from rest_framework.filters import OrderingFilter from users.users_filters import ( UserFilter, UserOperateFilter, UserLoginOperateFilter ) from utils.common.paginations import PageNumberPager from users.users_serializers import ( UserSerializer, JwtSerializer, OperateLogSerializer, UserUpdatePasswordSerializer, UserLoginOperateSerializer ) import ipware import ipaddress import logging from django.utils import timezone from utils.plugin.crypto import decrypt_rsa from utils.plugin.captcha import check_code logger = logging.getLogger("server") class UsersView(ListModelMixin, RetrieveModelMixin, CreateModelMixin, DestroyModelMixin, UpdateModelMixin, GenericViewSet): """ list: 查询用户列表 retrieve: 查询一个用户 create: 创建一个新用户 delete: 删除一个现有用户 update: 更新一个现有用户 partial_update: 更新一个现有用户的一个或多个字段 """ queryset = UserProfile.objects.all().order_by("id") serializer_class = UserSerializer pagination_class = PageNumberPager # 过滤字段 filter_backends = (DjangoFilterBackend,) filter_class = UserFilter # 操作描述信息 get_description = "获取用户" post_description = "新建用户" put_description = "更新用户" delete_description = "删除用户" class OperateLogView(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ list: 查询操作记录列表 retrieve: 查询一条操作记录 """ queryset = OperateLog.objects.all().order_by("-create_time") serializer_class = OperateLogSerializer pagination_class = PageNumberPager filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = UserOperateFilter ordering_fields = ("create_time", "username", "request_ip") # 操作描述信息 get_description = "获取用户操作记录" class UserLoginOperateView(ListModelMixin, RetrieveModelMixin, GenericViewSet): """ list: 查询操作记录列表 retrieve: 查询一条操作记录 """ queryset = UserLoginLog.objects.all().exclude( username="匿名用户").order_by("-login_time") serializer_class = UserLoginOperateSerializer pagination_class = PageNumberPager filter_backends = (DjangoFilterBackend, OrderingFilter) filter_class = UserLoginOperateFilter ordering_fields = ("login_time", "username", "ip", "role") # 操作描述信息 get_description = "获取用户操作记录" class JwtAPIView(JSONWebTokenAPIView): """ post: 登录,签发 JwtToken 令牌 """ serializer_class = JwtSerializer @staticmethod def validate_ip(str_ip): """ 校验ip格式 暂时不用 """ try: ipaddress.ip_address(str_ip) return True except ValueError: pass return False @staticmethod def _get_login_log(request): """ 创建登陆记录 暂时不用 """ login_ip, routeable = ipware.get_client_ip(request) data = { 'username': request.data.get("username", ""), 'login_time': timezone.now(), 'role': "超级管理员用户", 'ip': login_ip } try: if not (login_ip and JwtAPIView.validate_ip(login_ip)): data.update({'ip': login_ip[:15]}) UserLoginLog.objects.create(**data) except Exception as e: logger.error("Create login log error: {}".format(e)) def post(self, request, *args, **kwargs): code = request.data.get("code", "") if code.lower() != request.session.get("valid_code", "").lower(): raise ValidationError("code error") # django authenticate 缺陷,验证 username 大小写不敏感 username = decrypt_rsa(request.data.get("username", "")) password = decrypt_rsa(request.data.get("password", "")) if not UserProfile.objects.filter(username=username).exists(): raise ValidationError(f"{username} dose not exists.") if re.search(r"\s", password): raise ValidationError( "Unable to log in with provided credentials.") serializer = self.get_serializer( data={ "username": username, "password": password, "remember": request.data.get("remember") } ) if not serializer.is_valid(): raise ValidationError( "Unable to log in with provided credentials.") user = serializer.object.get("user") or request.user token = serializer.object.get("token") response_data = api_settings.JWT_RESPONSE_PAYLOAD_HANDLER( token, user, request) response = Response(response_data) # self._get_login_log(request) if api_settings.JWT_AUTH_COOKIE: # remember 取值 True,则 cookie 过期时间为 7 天 expiration_time = api_settings.JWT_EXPIRATION_DELTA if serializer.validated_data.get("remember"): expiration_time = datetime.timedelta(days=7) expiration = (datetime.datetime.utcnow() + expiration_time) response.set_cookie( api_settings.JWT_AUTH_COOKIE, token, expires=expiration, ) return response class UserUpdatePasswordView(GenericViewSet, CreateModelMixin): """ create: 修改用户密码 """ queryset = UserProfile.objects.all() serializer_class = UserUpdatePasswordSerializer # 操作描述 post_description = "更新用户密码" class CaptchaView(GenericViewSet, ListModelMixin): """ list: 获取图形验证码 """ authentication_classes = () permission_classes = () def list(self, request, *args, **kwargs): # 调用函数生成图片验证码 image_obj, code = check_code() # 将验证码字符存入session request.session["valid_code"] = code request.session.set_expiry(60) stream = BytesIO() image_obj.save(stream, "png") # 将从内存空间获取的图片验证码传给前端 return HttpResponse(stream.getvalue()) ================================================ FILE: omp_server/utils/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jon.liu@yunzhihui.com # Create time: 2021-09-10 16:21 # IDE: PyCharm # Version: 1.0 # Introduction: ================================================ FILE: omp_server/utils/common/__init__.py ================================================ ================================================ FILE: omp_server/utils/common/exceptions.py ================================================ """ 公共异常 注意 common_exception_handler 的优先级按照以下顺序进行: 1. GeneralError 异常实例 2. FORMAT_ERRORS 字典,自定义格式化函数处理的错误 3. ERROR_MESSAGE 字典,含描述信息的错误 4. CODE_MESSAGE 字典,含描述信息的状态码 将返回的结果注入 response 的 message 字段中,缺省值为 "后端程序错误" """ from django.db import DatabaseError from rest_framework.exceptions import ValidationError class GeneralError(Exception): """ 通用错误 """ def __init__(self, err="通用异常"): super(GeneralError, self).__init__(err) class OperateError(GeneralError): """ 操作错误 """ def __init__(self, err="操作发生错误"): super(OperateError, self).__init__(err) def _validation_error_message(exc, response): """ ValidationError 错误数据格式化 """ err_message = "数据校验错误" assert response.data is not None data = response.data if isinstance(data, list): err_message = "; ".join(data) if isinstance(data, dict): err_message_ls = [] for k, v in data.items(): if isinstance(v, list): ip_err = "Enter a valid IPv4 or IPv6 address." if ip_err in v: v[v.index(ip_err)] = "IP格式不合法" err_message_ls.append("; ".join(v)) else: err_message_ls.append(v) err_message = "; ".join(err_message_ls) return err_message # 自定义格式化函数处理的错误 FORMAT_ERRORS = { ValidationError: _validation_error_message, } # 含描述信息的错误 ERROR_MESSAGE = { NameError: "变量未被定义或引用", DatabaseError: "数据库错误", } # 含描述信息的状态码 CODE_MESSAGE = { 401: "未认证", 403: "无访问权限", 404: "未找到", 405: "暂不支持此请求", 500: "服务器错误", } ================================================ FILE: omp_server/utils/common/paginations.py ================================================ """ 公共分页器 """ from rest_framework.pagination import ( PageNumberPagination, LimitOffsetPagination, CursorPagination ) class PageNumberPager(PageNumberPagination): """ 容量分页 """ page_size = 10 max_page_size = 100 page_query_param = 'page' page_size_query_param = 'size' class LimitOffsetPager(LimitOffsetPagination): """ 偏移分页 """ default_limit = 10 max_limit = 100 limit_query_param = 'limit' offset_query_param = 'offset' class CursorPager(CursorPagination): """ 游标分页 """ page_size = 10 cursor_query_param = 'cursor' ================================================ FILE: omp_server/utils/common/serializers.py ================================================ """ 公共序列化器 """ from rest_framework import serializers from rest_framework.exceptions import ValidationError from db_models import models from db_models.models import Host, UploadFileHistory class HostIdsSerializer(serializers.Serializer): """ 主机 id 列表序列化类 """ host_ids = serializers.ListField( help_text="主机 ID 列表", required=True, error_messages={"required": "必须包含[host_ids]字段"}, allow_empty=False) def validate_host_ids(self, host_ids): """ 校验主机 ID 列表中主机是否都存在 """ exists_ids = Host.objects.filter( id__in=host_ids).values_list("id", flat=True) diff = set(host_ids) - set(exists_ids) if diff: raise ValidationError( f"主机列表中有不存在的ID [" f"{','.join(map(lambda x: str(x), diff))}" f"]") return host_ids class UploadFileSerializer(serializers.Serializer): file = serializers.FileField( help_text="上传的文件", required=True, error_messages={"required": "必须包含[file]字段"} ) storage_klass = serializers.CharField( help_text="存储方式", required=False, default="location") module = serializers.CharField( help_text="需要上传文件的model", required=False, default="") module_id = serializers.IntegerField( help_text="需要上传文件的model id", required=False, default=0) file_name = serializers.CharField( help_text="保存后文件名", required=False, default="") file_url = serializers.CharField( help_text="保存后文件访问路径", required=False, default="") union_id = serializers.CharField( help_text="文件union_id", required=False, default="") def validate(self, attrs): module = attrs.get("module") module_id = attrs.get("module_id") if module: _obj = getattr(models, module).objects.filter(id=module_id).first() if not _obj: raise ValidationError({ "module": "请确定指定上传文件的module" }) setattr(self, "module_obj", _obj) if attrs.get("storage_klass", "location") \ not in UploadFileHistory.STORAGE_KLASS: raise ValidationError({ "storage_klass": f"文件存储方式目前只支持{UploadFileHistory.STORAGE_KLASS}" }) return attrs def create(self, validated_data): user = self.context.get("request").user file = validated_data.get("file") if hasattr(self, "module_obj"): module_obj = self.module_obj else: module_obj = None obj = getattr( UploadFileHistory, validated_data.get("storage_klass", "location") )(file, module_obj, user) validated_data.update( { "file_name": obj.file_name, "union_id": obj.union_id, "file_url": obj.file_url } ) return validated_data class DynamicFieldsModelSerializer(serializers.ModelSerializer): """ A ModelSerializer that takes an additional `fields` argument that controls which fields should be displayed. """ def __init__(self, *args, **kwargs): # Don't pass the 'fields' arg up to the superclass fields = kwargs.pop('fields', None) # 正常地实例化父类 super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs) if fields is not None: # 删除fields参数中未指定的任何字段 allowed = set(fields) existing = set(self.fields) for field_name in existing - allowed: self.fields.pop(field_name) ================================================ FILE: omp_server/utils/common/urls.py ================================================ from rest_framework.routers import DefaultRouter from utils.common.views import UploadFileAPIView router = DefaultRouter() router.register(r'upload_file', UploadFileAPIView, basename="upload_file") urlpatterns = router.urls ================================================ FILE: omp_server/utils/common/validators.py ================================================ """ 公共验证器 """ import re import emoji from rest_framework.exceptions import ValidationError class ReValidator: """ 正则表达式验证器 """ requires_context = True def __init__(self, regex, message="格式不合法"): self.regex = regex self.message = message def __call__(self, value, serializer_field): if re.match(self.regex, value) is None: field = serializer_field.field_name if serializer_field.help_text is not None: field = serializer_field.help_text raise ValidationError(f"{field}{self.message}") class NoEmojiValidator: """ 表情验证器 """ requires_context = True def __init__(self, message="不可含有表情"): self.message = message def __call__(self, value, serializer_field): if emoji.emoji_count(value) > 0: field = serializer_field.field_name if serializer_field.help_text is not None: field = serializer_field.help_text raise ValidationError(f"{field}{self.message}") class NoChineseValidator: """ 中文验证器 """ requires_context = True def __init__(self, message="不可含有中文"): self.message = message def __call__(self, value, serializer_field): if any(re.findall(r"[\u4e00-\u9fa5]?", value)): field = serializer_field.field_name if serializer_field.help_text is not None: field = serializer_field.help_text raise ValidationError(f"{field}{self.message}") class UserPasswordValidator: """ 用户密码验证器 """ requires_context = True def __init__(self, message="格式不合法"): self.char_list = ("`", "~", "!", "?", "@", "#", "$", "%", "^", "&", ",", "(", ")", "[", "]", "{", "}", "_", "+", "-", "*", "/", ".", ";", ":") self.message = message def __call__(self, value, serializer_field): for e in value: if re.match(r"[a-zA-Z0-9]", str(e)) is not None: continue if str(e) not in self.char_list: field = serializer_field.field_name if serializer_field.help_text is not None: field = serializer_field.help_text raise ValidationError(f"{field}{self.message}") ================================================ FILE: omp_server/utils/common/views.py ================================================ import os import logging from django.conf import settings from django.http import FileResponse from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import ListModelMixin, CreateModelMixin from db_models.models import UploadFileHistory from utils.common.exceptions import OperateError from utils.common.serializers import UploadFileSerializer logger = logging.getLogger("server") class BaseDownLoadTemplateView(GenericViewSet, ListModelMixin): """ list: 获取模板文件 """ # 操作描述信息 get_description = "获取模板文件" def list(self, request, *args, **kwargs): template_file_name = kwargs.get("template_file_name") assert template_file_name is not None parent_path = kwargs.get("parent_path", "template") template_path = os.path.join( settings.BASE_DIR.parent, "package_hub", parent_path, template_file_name) try: file = open(template_path, 'rb') response = FileResponse(file) response["Content-Type"] = "application/octet-stream" response["Content-Disposition"] = \ f"attachment;filename={template_file_name}" except FileNotFoundError: logger.error(f"{template_path} not found") raise OperateError("组件模板文件缺失") return response class UploadFileAPIView(GenericViewSet, CreateModelMixin): post_description = "上传文件" queryset = UploadFileHistory.objects.all() serializer_class = UploadFileSerializer ================================================ FILE: omp_server/utils/exception_handler.py ================================================ # -*- coding: utf-8 -*- # Project: exception_handler # Author: jon.liu@yunzhihui.com # Create time: 2021-09-10 16:32 # IDE: PyCharm # Version: 1.0 # Introduction: """ 整个项目内的异常处理方法 """ import logging import traceback from rest_framework.views import exception_handler from rest_framework.response import Response from utils.common.exceptions import ( GeneralError, FORMAT_ERRORS, ERROR_MESSAGE, CODE_MESSAGE ) logger = logging.getLogger("server") class ExceptionResponse: def __init__(self, exc, context): self.exc = exc self.context = context def err_response(self): logger.error(f"ExceptionResponse: {str(self.exc)}; {self.context}") logger.error(f"ExceptionResponse: {traceback.format_exc()};") response = exception_handler(self.exc, self.context) response_status_code = 200 if response: response_status_code = response.status_code message = "后端程序错误" error_response = Response( status=200, data={ "code": 1, "data": None }) # GeneralError 异常实例 if isinstance(self.exc, GeneralError): message = str(self.exc) # 自定义格式化函数处理的错误 elif type(self.exc) in FORMAT_ERRORS: error_format_fun = FORMAT_ERRORS.get(type(self.exc)) message = error_format_fun(self.exc, response) # 含描述信息的错误 elif type(self.exc) in ERROR_MESSAGE: message = ERROR_MESSAGE.get(type(self.exc)) # 含描述信息的状态码 elif response_status_code in CODE_MESSAGE: message = CODE_MESSAGE.get(response_status_code) error_response.data["message"] = message return error_response def common_exception_handler(exc, context): """ 异常处理函数 :param exc: :param context: :return: """ return ExceptionResponse(exc, context).err_response() ================================================ FILE: omp_server/utils/middleware_handler.py ================================================ # -*- coding: utf-8 -*- # Project: middleware_handler # Author: jon.liu@yunzhihui.com # Create time: 2021-09-11 16:48 # IDE: PyCharm # Version: 1.0 # Introduction: """ 公共中间件 """ import json import ipware import logging from django.urls import resolve from django.utils.deprecation import MiddlewareMixin from django.http import JsonResponse from rest_framework.reverse import reverse from rest_framework_jwt.utils import jwt_decode_handler from jwt import DecodeError from db_models.models import ( OperateLog, UserLoginLog, UserProfile ) from django.utils import timezone from omp_server.settings import INTERFACE_KINDS logger = logging.getLogger("server") def get_username_of_token(token): """通过jwt token 解析username""" _token_user = jwt_decode_handler(token) _username = _token_user.get("username") return _username USER_TO_ROLE_EN_DICT = { "superuser": "普通管理员", "readonlyuser": "只读用户" } class OperationLogMiddleware(MiddlewareMixin): """用于处理操作日志的中间件""" def process_response(self, request, response): """ 拦截请求的中间件 :param request: 请求对象 :type request HttpRequest :param response: 响应对象 :return: """ _method = request.method.lower() if _method == "get": return response _url = request.path if _url.startswith("/proxy/v1/grafana"): return response view_class = resolve(_url).func.cls if hasattr(view_class, f"{_method}_description"): _desc = getattr(view_class, f"{_method}_description") else: _desc = "无法确定用户行为" _ip, _ = ipware.get_client_ip(request) try: token = request.COOKIES.get("jwtToken", "toke") _username = get_username_of_token(token) except DecodeError: _username = "匿名用户" if _url == reverse("login"): _desc = "用户登录" request_result = "" _token = None if "token" in response.data: request_result = "登录成功" _token = response.data.get("token", "") _username = get_username_of_token(_token) else: return response _token_user = UserProfile.objects.filter(username=_username).first() data = { 'username': _username, 'login_time': timezone.now(), 'role': USER_TO_ROLE_EN_DICT.get(_token_user.role.lower(), ""), 'ip': _ip, 'request_result': request_result } UserLoginLog.objects.create(**data) else: # 读取已封装响应数据 try: res_data = json.loads(response.rendered_content) except Exception as e: return response method_dc = { 'put': '修改', 'get': '查看', 'delete': '删除', 'trace': '查看', 'patch': '修改' } method_st = method_dc.get(_method) if not method_st: method_st = INTERFACE_KINDS.get(_url, "修改") if _username != "匿名用户": OperateLog( username=_username, request_ip=_ip, request_method=method_st, request_url=_url, description=_desc, response_code=res_data.get("code", 0), request_result=res_data.get("message", "") ).save() return response class RoleAuthenticationMiddleware(MiddlewareMixin): """用户角色访问限制""" def process_view(self, request, func, *args, **kwargs): _method = request.method.lower() if _method == "get": return None _url = request.path if _url.startswith("/proxy/v1/grafana") or _url.startswith("/api/login/"): return None try: token = request.COOKIES.get("jwtToken", "toke") _token_user = jwt_decode_handler(token) _username = _token_user.get("username") except DecodeError: _username = "匿名用户" _token_user = UserProfile.objects.filter(username=_username).first() if not _token_user: # 非页面访问omp接口,放行 return None if _token_user.role.lower() == "superuser": return None logger.error(f"{_token_user} prohibited this action") return JsonResponse({"code": 1, "data": None, "message": f"该{_token_user.username}用户无权限"}) ================================================ FILE: omp_server/utils/parse_config.py ================================================ # -*- coding: utf-8 -*- # Project: parse_config # Author: jon.liu@yunzhihui.com # Create time: 2021-09-15 09:26 # IDE: PyCharm # Version: 1.0 # Introduction: """ 解析配置文件 """ import os from ruamel import yaml project_path = os.path.dirname( os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ) config_file_path = os.path.join(project_path, "config/omp.yaml") private_key_path = os.path.join(project_path, "config/private_key.pem") # openssl genrsa -out private_key.pem 2048 # openssl rsa -in private_key.pem -out public_key.pem -pubout with open(private_key_path, "r") as key_f: PRIVATE_KEY = key_f.read() with open(config_file_path, "r") as fp: CONFIG_DIC = yaml.load(fp, Loader=yaml.SafeLoader) GLOBAL_RUNUSER = CONFIG_DIC.get("global_runuser") LOCAL_IP = CONFIG_DIC.get("local_ip") TENGINE_PORT = CONFIG_DIC.get("tengine") SSH_CMD_TIMEOUT = CONFIG_DIC.get("ssh_cmd_timeout", 60) SSH_CHECK_TIMEOUT = CONFIG_DIC.get("ssh_check_timeout", 10) THREAD_POOL_MAX_WORKERS = CONFIG_DIC.get("thread_pool_max_workers", 20) SALT_RET_PORT = CONFIG_DIC.get("salt_master", {}).get("ret_port", 19005) TOKEN_EXPIRATION = CONFIG_DIC.get("token_expiration", 1) MONITOR_PORT = CONFIG_DIC.get("monitor_port") GRAFANA_API_KEY = CONFIG_DIC.get("grafana_api_key") PROMETHEUS_AUTH = CONFIG_DIC.get("prometheus_auth", {}) GRAFANA_AUTH = CONFIG_DIC.get("grafana_auth", {}) LOKI_CONFIG = CONFIG_DIC.get("loki_config", {}) HEALTH_REDIS_TIMEOUT = int(CONFIG_DIC.get("health_redis_timeout", 60)) * 60 HEALTH_REQUEST_COUNT = int(CONFIG_DIC.get("health_request_count", 6)) HEALTH_REQUEST_SLEEP = int(CONFIG_DIC.get("health_request_sleep", 5)) CLEAR_DB = CONFIG_DIC.get("clear_table", {}) SERVICE_DISCOVERY = CONFIG_DIC.get("service_discovery", []) OMP_REDIS_HOST = os.getenv( "OMP_REDIS_HOST", CONFIG_DIC.get("redis", {}).get("host") ) OMP_REDIS_PORT = os.getenv( "OMP_REDIS_PORT", CONFIG_DIC.get("redis", {}).get("port") ) OMP_REDIS_PASSWORD = os.getenv( "OMP_REDIS_PASSWORD", CONFIG_DIC.get("redis", {}).get("password") ) OMP_MYSQL_HOST = os.getenv( "OMP_MYSQL_HOST", CONFIG_DIC.get("mysql", {}).get("host") ) OMP_MYSQL_PORT = os.getenv( "OMP_MYSQL_PORT", CONFIG_DIC.get("mysql", {}).get("port") ) OMP_MYSQL_USERNAME = os.getenv( "OMP_MYSQL_USERNAME", CONFIG_DIC.get("mysql", {}).get("username") ) OMP_MYSQL_PASSWORD = os.getenv( "OMP_MYSQL_PASSWORD", CONFIG_DIC.get("mysql", {}).get("password") ) BASIC_ORDER = CONFIG_DIC.get("basic_order", {}) AFFINITY_FIELD = CONFIG_DIC.get("affinity", {}) HADOOP_ROLE = CONFIG_DIC.get("hadoop_role", {}) HOSTNAME_PREFIX = CONFIG_DIC.get("hostname_prefix", {}) BACKUP_SERVICE = CONFIG_DIC.get("backup_service", []) ================================================ FILE: omp_server/utils/plugin/__init__.py ================================================ ================================================ FILE: omp_server/utils/plugin/agent_util.py ================================================ # -*- coding: utf-8 -*- # Project: agent_util # Author: jon.liu@yunzhihui.com # Create time: 2021-05-10 20:13 # IDE: PyCharm # Version: 1.0 # Introduction: """ 安装主机Agent的方法 """ import os import yaml from celery.utils.log import get_task_logger from utils.plugin.ssh import SSH from omp_server.settings import PROJECT_DIR from utils.parse_config import LOCAL_IP from utils.parse_config import SALT_RET_PORT logger = get_task_logger("celery_log") class Agent(object): """agent安装管理""" def __init__(self, host, port, username, password, install_dir): """ agent管理需要的初始化文件 :param host: 主机ip :param port: 主机端口 :param username: 主机用户名 :param password: 主机密码 :param install_dir: 安装路径 """ logger.info( f"Agent Manager: {host}; {port}; {username}; {install_dir}") self.host = host self.port = port self.username = username self.password = password self.run_user = username self.master_ip = LOCAL_IP self.master_port = SALT_RET_PORT self.install_dir = install_dir self.agent_name = "omp_salt_agent" self.package_hub = os.path.join(PROJECT_DIR, "package_hub") self.agent_file_path = os.path.join( self.package_hub, "omp_salt_agent.tar.gz") self.ssh = SSH( hostname=self.host, port=int(self.port), username=self.username, password=self.password ) def generate_conf(self): """ 生成agent的配置文件 :return: """ try: logger.info(f"Generate Conf For {self.host}!") agent_conf_dic = { "master": self.master_ip, "master_port": self.master_port, "user": self.run_user, "id": self.host, "root_dir": os.path.join(self.install_dir, f"{self.agent_name}/data/"), "conf_file": os.path.join(self.install_dir, f"{self.agent_name}/conf/minion"), "rejected_retry": True } with open(os.path.join(self.package_hub, self.host, "minion"), "w") as fp: yaml.dump(agent_conf_dic, fp, Dumper=yaml.SafeDumper) return True, "generate success." except Exception as error: return False, str(error) def agent_deploy(self): """ 安装agent :return: """ logger.info(f"deploy host for agent {self.host}!") # step1: 判断是否可连接 ssh_state, _ = self.ssh.check() if not ssh_state: return False, "ssh connect failed" # 删除原有omp_salt_agent logger.info(f"delete origin omp_salt_agent for {self.host}") omp_salt = os.path.join(self.install_dir, "omp_salt_agent") _delete_cron_cmd = f"sed -i '/omp_salt_agent/d' /var/spool/cron/{self.username}; " # _stop_agent = f"bash {omp_salt}/bin/omp_salt_agent stop; rm -rf {omp_salt}" _stop_agent = f"bash {omp_salt}/bin/omp_salt_agent stop; /bin/rm -rf {omp_salt}/data/*" final_cmd = f"{_delete_cron_cmd} {_stop_agent}" self.ssh.cmd(final_cmd, timeout=60) # step2: push agent.tar.gz to remote host and install logger.info(f"push omp_salt_agent.tar.gz to {self.host}!") tar_push_state, tar_push_msg = self.ssh.file_push( self.agent_file_path, self.install_dir) if not tar_push_state: logger.error( f"file push to {self.host} with error: {tar_push_msg}") return False, tar_push_msg # step3: install agent logger.info(f"execute install command for {self.host}!") command = "cd {0} && tar xmf {1}.tar.gz && chown -R {2}:{2} {1} && rm -f {1}.tar.gz".format( self.install_dir, self.agent_name, self.run_user) cmd_exec_state, cmd_exec_msg = self.ssh.cmd(command) if not cmd_exec_state: logger.error( f"Error while install agent for {self.host}: {cmd_exec_msg}") return False, cmd_exec_msg # step4: make package_hub/host_ip/ logger.info(f"push config to {self.host}!") config_tmp_dir = os.path.join(self.package_hub, self.host) if not os.path.isdir(config_tmp_dir): os.makedirs(config_tmp_dir) config_tmp = os.path.join(config_tmp_dir, 'minion') config_gen_state, config_gen_msg = self.generate_conf() if not config_gen_state: logger.error( f"Error while generate conf file for agent {self.host}: {config_gen_msg}") return False, config_gen_msg # step5: push config file to remote host config_push_state, config_push_msg = self.ssh.file_push( config_tmp, os.path.join(self.install_dir, f'{self.agent_name}/conf/') ) if not config_push_state: logger.error( f"Error while send agent config to {self.host}: {config_push_msg}") return False, config_push_msg # step6: make and push scripts logger.info(f"push script to {self.host}!") with open(os.path.join(PROJECT_DIR, "scripts/source/omp_salt_agent"), "r") as fp: _script_content = fp.read() with open(os.path.join(config_tmp_dir, "omp_salt_agent"), "w") as fp: _content = _script_content.replace( "UNIQUE_INSTALL_DIR_FLAG", self.install_dir) _content = _content.replace("RUNUSER", self.run_user) fp.write(_content) script_push_state, script_push_msg = self.ssh.file_push( os.path.join(config_tmp_dir, "omp_salt_agent"), os.path.join(self.install_dir, '{}/bin/'.format(self.agent_name)) ) if not script_push_state: logger.error( f"Error while send agent script for {self.host}: {script_push_msg}") return False, script_push_msg # shutil.rmtree(config_tmp_dir) # step7: start and init agent logger.info(f"init omp_salt_agent for {self.host}!") command = f"cd {self.install_dir} && " \ f"chown -R {self.run_user}.{self.run_user} {self.agent_name} && " \ f"bash {self.agent_name}/bin/{self.agent_name} init" cmd_exec_state, cmd_exec_msg = self.ssh.cmd(command, timeout=120) if not cmd_exec_state: if "INIT_OMP_SALT_AGENT_SUCCESS" in cmd_exec_msg: return True, "agent deploy success" logger.error( f"Error while start agent for {self.host}: {cmd_exec_msg}") return False, cmd_exec_msg logger.info(f"success deploy agent for {self.host}!") return True, "agent deploy success." def agent_manage(self, action, install_app_dir): """ manage salt agent, start stop status :param str action: [start|stop|status|restart] :param str install_app_dir: e.g. /data/app :return: the (state, status) , as a 2-tuple """ if action == "start": command = "cd {}/{} && bash ./bin/omp_salt_agent start".format( install_app_dir, self.agent_name) elif action == "stop": command = "cd {}/{} && bash ./bin/omp_salt_agent stop".format( install_app_dir, self.agent_name) elif action == "status": command = "cd {}/{} && bash ./bin/omp_salt_agent status".format( install_app_dir, self.agent_name) elif action == "restart": command = "cd {}/{} && bash ./bin/omp_salt_agent restart".format( install_app_dir, self.agent_name) else: return False, "start|stop|status|restart" cmd_exec_state, cmd_exec_msg = self.ssh.cmd(command) if cmd_exec_state: return True, cmd_exec_msg[0].strip() else: return False, cmd_exec_msg if __name__ == '__main__': # test_agent = Agent( # host="10.0.7.146", # port=36000, # username="root", # password="yunzhihui123", # run_user="root", # install_dir="/data/app" # ) # flag, message = test_agent.agent_deploy() # print(flag, message) pass ================================================ FILE: omp_server/utils/plugin/captcha/__init__.py ================================================ from .captcha import check_code __all__ = [ check_code ] ================================================ FILE: omp_server/utils/plugin/captcha/captcha.py ================================================ import os import random from PIL import Image, ImageDraw, ImageFilter, ImageFont font_path = os.path.join( os.path.dirname(__file__), "monaco.ttf" ) def check_code(width=120, height=38, char_length=4, font_file=font_path, font_size=32): code = [] img = Image.new(mode='RGB', size=(width, height), color=(255, 255, 255)) draw = ImageDraw.Draw(img, mode='RGB') def rndChar(): """ 生成随机字母 :return: """ return chr(random.randint(65, 90)) def rndColor(): """ 生成随机颜色 :return: """ return (random.randint(0, 255), random.randint(10, 255), random.randint(64, 255)) # 写文字 font = ImageFont.truetype(font_file, font_size) for i in range(char_length): char = rndChar() code.append(char) h = random.randint(0, 4) draw.text([i * width / char_length, h], char, font=font, fill=rndColor()) # 写干扰点 for i in range(40): draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor()) # 写干扰圆圈 for i in range(40): draw.point([random.randint(0, width), random.randint(0, height)], fill=rndColor()) x = random.randint(0, width) y = random.randint(0, height) draw.arc((x, y, x + 4, y + 4), 0, 90, fill=rndColor()) # 画干扰线 for i in range(5): x1 = random.randint(0, width) y1 = random.randint(0, height) x2 = random.randint(0, width) y2 = random.randint(0, height) draw.line((x1, y1, x2, y2), fill=rndColor()) img = img.filter(ImageFilter.EDGE_ENHANCE_MORE) return img, ''.join(code) if __name__ == '__main__': image_obj, code = check_code() image_obj.show() # 直接打开 ================================================ FILE: omp_server/utils/plugin/crontab_utils.py ================================================ # -*- coding: utf-8 -*- # Project: crontab_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-14 11:35 # IDE: PyCharm # Version: 1.0 # Introduction: """ 定时周期性任务设置方法 """ import json import pytz from copy import deepcopy import logging from django_celery_beat.models import PeriodicTask from django_celery_beat.models import CrontabSchedule from django_celery_beat.models import IntervalSchedule from django.core.exceptions import ObjectDoesNotExist from functools import wraps from omp_server.settings import TIME_ZONE from db_models.models import Maintain logger = logging.getLogger("server") class CrontabUtils(object): """ 定时、周期性任务创建工具 """ def __init__( self, task_name=None, task_func="", task_args=None, task_kwargs=None, task_timeout=None): """ 定时任务初始化方法 :param task_name: 任务名称,全局唯一 :param task_func: 任务执行方法 :param task_args: 任务执行位置参数 :param task_kwargs: 任务执行关键字参数 :param task_timeout: 任务超时时间(暂时未定) """ self.task_name = task_name self.task_func = task_func self.task_args = task_args if task_args else () self.task_kwargs = task_kwargs if task_kwargs else {} self.task_time = task_timeout self.create_task_obj_dic = { "name": self.task_name, "task": self.task_func, "args": json.dumps(self.task_args), "kwargs": json.dumps(self.task_kwargs) } def create_crontab_job( self, minute="*", hour="*", day_of_month="*", month_of_year="*", day_of_week="*", job_timezone=TIME_ZONE): """ 创建定时任务 如果定时任务正在执行,但是在指定频率内未完成,那么新任务仍能下发 :param minute: 分钟 :param hour: 小时 :param day_of_month: 每月的第几天 :param month_of_year: 每年的第几个月 :param day_of_week: 每周的第几天 :param job_timezone: 时区 :return: (flag, msg) """ if self.check_task_exist(): return False, f"{self.task_name} already exist!" _schedule, _created = CrontabSchedule.objects.get_or_create( minute=minute, hour=hour, day_of_week=day_of_week, day_of_month=day_of_month, month_of_year=month_of_year, timezone=pytz.timezone(job_timezone) ) _dic = deepcopy(self.create_task_obj_dic) _dic.update({"crontab": _schedule}) job_obj = PeriodicTask.objects.create(**_dic) return True, job_obj.name def create_internal_job(self, num, unit_type="minutes"): """ 创建周期性任务 周期类型有 DAYS = DAYS HOURS = HOURS MINUTES = MINUTES SECONDS = SECONDS :param num: 周期时长 :param unit_type: 周期类型 :return: """ if self.check_task_exist(): return False, f"{self.task_name} already exist!" if not num or not isinstance(num, int): return False, "num must be integer" period = getattr(IntervalSchedule, unit_type.upper(), None) if not period: return False, "unit_type must be one of DAYS/HOURS/MINUTES/SECONDS" _schedule, _created = IntervalSchedule.objects.get_or_create( every=10, period=IntervalSchedule.SECONDS ) _dic = deepcopy(self.create_task_obj_dic) _dic.update({"interval": _schedule}) job_obj = PeriodicTask.objects.create(**_dic) return True, job_obj.name def check_task_exist(self): """ 检查task是否存在,从task name来 :return: """ return PeriodicTask.objects.filter(name=self.task_name).exists() def delete_job(self): """ 删除定时任务 :return: """ try: PeriodicTask.objects.get(name=self.task_name).delete() return True, "success" except ObjectDoesNotExist: return False, f"{self.task_name} not exists!" def change_task(task_id, data=dict()): task_func = data.get("task_func", "backups.tasks.backup_service") task_name = data.get("task_name", f"backups_cron_task_{task_id}") try: cron_args = data.get("crontab_detail", {}) hour = cron_args.get("hour", "") hour_ls = str(hour).split("/") if len(hour_ls) == 2: cron_args["hour"] = ','.join( list( map(str, range(0, 24, int(hour_ls[1]))) )) create = data.get("is_on", False) cron_obj = CrontabUtils(task_name=task_name, task_func=task_func, task_kwargs={"task_id": task_id}) if cron_obj.check_task_exist(): res, msg = cron_obj.delete_job() logger.info(f"删除周期任务{task_name},结果{res},详情{msg}") if create: res, msg = cron_obj.create_crontab_job(**cron_args) logger.info(f"创建周期任务{task_name},结果{res},详情{msg}") return 0, "" except Exception as e: logger.error(f"执行定时任务失败:{str(e)}") return 1, "执行定时任务失败,请重试!" def get_per_cron(cron_args): """ */int 进行拆除 """ max_time = { "hour": 24, "minute": 60, "day_of_month": 30, "day_of_week": 7, "month_of_year": 12 } for k, v in cron_args.items(): v_ls = str(v).split("/") if len(v_ls) == 2: cron_args[k] = ','.join( list( map(str, range(0, max_time.get(k), int(v_ls[1]))) )) return cron_args def change_task(task_id, data=dict()): task_func = data.get("task_func", "backups.tasks.backup_service") task_name = data.get("task_name", f"backups_cron_task_{task_id}") try: cron_args = data.get("crontab_detail", {}).copy() cron_args = get_per_cron(cron_args) create = data.get("is_on", False) cron_obj = CrontabUtils(task_name=task_name, task_func=task_func, task_kwargs={"task_id": task_id}) if cron_obj.check_task_exist(): res, msg = cron_obj.delete_job() logger.info(f"删除周期任务{task_name},结果{res},详情{msg}") if create: res, msg = cron_obj.create_crontab_job(**cron_args) logger.info(f"创建周期任务{task_name},结果{res},详情{msg}") return 0, "" except Exception as e: logger.error(f"执行定时任务失败:{str(e)}") return 1, "执行定时任务失败,请重试!" def maintain(f): @wraps(f) def decorated(*args, **kwargs): if Maintain.objects.filter(matcher_value="default").first(): logger.info("任务处于维护模式") return "task will not run" return f(*args, **kwargs) return decorated ================================================ FILE: omp_server/utils/plugin/crypto.py ================================================ import base64 import hashlib from Crypto.PublicKey import RSA from Crypto.Cipher import PKCS1_v1_5, AES from base64 import urlsafe_b64decode, urlsafe_b64encode from django.conf import settings class AESCryptor: """ AES 加密器 """ def __init__(self): self.__encryptKey = settings.SECRET_KEY self.__key = hashlib.md5(self.__encryptKey.encode("utf8")).digest() @staticmethod def pad(text, block_size=32): pad = block_size - (len(text) % block_size) return (text + pad * chr(pad)).encode("utf8") @staticmethod def un_pad(text): index = text[-1] return text[:-index].decode("utf8") def encode(self, plaintext): """ AES 加密 """ ciphertext = AES.new( self.__key, AES.MODE_ECB ).encrypt(self.pad(plaintext)) return urlsafe_b64encode(ciphertext).decode("utf8").rstrip("=") def decode(self, ciphertext): """ AES 解密 """ ciphertext = urlsafe_b64decode( ciphertext + "=" * (4 - len(ciphertext) % 4)) cipher = AES.new(self.__key, AES.MODE_ECB) return self.un_pad(cipher.decrypt(ciphertext)) def decrypt_rsa(encrypt_str, private_key=settings.PRIVATE_KEY): rsakey = RSA.importKey(private_key.encode()) cipher = PKCS1_v1_5.new(rsakey) st = base64.b64decode(encrypt_str.encode()) return cipher.decrypt(st, sentinel="").decode() ================================================ FILE: omp_server/utils/plugin/install_ntpdate.py ================================================ # -*- coding:utf-8 -*- # Project: instanll_ntpdate # Create time: 2022/2/23 1:32 下午 import os import yaml import logging from utils.plugin.salt_client import SaltClient from concurrent.futures import ThreadPoolExecutor from omp_server.settings import PROJECT_DIR logger = logging.getLogger("server") class InstallNtpdate(object): def __init__(self, host_obj_list=None): if not host_obj_list or not isinstance(host_obj_list, list): logger.error("host_obj_list must be type of list") raise TypeError("host_obj_list must be type of list!") self.host_obj_list = host_obj_list self.salt_client = SaltClient() self.name = "ntpdate" self.ntpdate_package_name = "" self.config_path = os.path.join(PROJECT_DIR, "config/omp.yaml") self.package_hub_dir = os.path.join(PROJECT_DIR, "package_hub") self.check_ntpdate_package() def check_ntpdate_package(self): """检查ntpdate源码包是否存在""" for file in os.listdir(self.package_hub_dir): if file.startswith(self.name) and file.endswith("tar.gz"): self.ntpdate_package_name = file break def get_config_dic(self): """ 获取配置文件详细信息 :return: """ with open(self.config_path, "r", encoding="utf8") as fp: return yaml.load(fp, Loader=yaml.FullLoader) def get_run_user(self): """获取run_user""" return self.get_config_dic().get("global_user") @staticmethod def execute(host_obj_lst, thread_name_prefix, func): """ 执行函数 :param host_obj_lst: 主机对象组成的列表 :param thread_name_prefix: 线程前缀 :param func: 执行函数对象, 仅接收host主机对象参数 :return: """ thread_p = ThreadPoolExecutor( max_workers=20, thread_name_prefix=thread_name_prefix) # futures_list:[(ip, future)] futures_list = list() for item in host_obj_lst: future = thread_p.submit(func, item) futures_list.append((item.ip, future)) # result_list:[(ip, res_bool, res_msg), ...] result_list = list() for f in futures_list: result_list.append((f[0], f[1].result()[0], f[1].result()[1])) thread_p.shutdown(wait=True) error_msg = "" for el in result_list: if not el[1]: error_msg += \ f"{el[0]}: (execute_flag: {el[1]}; execute_msg: {el[2]});" if error_msg: return False, error_msg return True, "success!" def _install_ntpdate(self, host_obj): """安装ntpdate""" if not self.ntpdate_package_name: logger.error("ntpdate_package not existed!") return False, "ntpdate_package not existed!" host_obj.ntpdate_install_status = 2 host_obj.save() agent_dir = host_obj.agent_dir app_dir = os.path.join(agent_dir, "app") target_package_path = os.path.join(app_dir, self.ntpdate_package_name) data_dir = os.path.join(agent_dir, "appData") log_dir = os.path.join(agent_dir, "logs") ntpdate_cron_path = os.path.join(app_dir, f"{self.name}/scripts/{self.name}_cron.sh") scripts_path = os.path.join(app_dir, f"{self.name}/scripts/{self.name}") # 1.发包 send_flag, send_msg = self.salt_client.cp_file( target=host_obj.ip, source_path=self.ntpdate_package_name, target_path=target_package_path, makedirs=True ) logger.info( f"send ntpdate packages,send_flag: {send_flag}; send_msg: {send_msg}" ) if not send_flag: return send_flag, send_msg # 2.解压缩与安装、启动 cmd_flag, cmd_msg = self.salt_client.cmd( target=host_obj.ip, command=f"(test -d {app_dir}|| mkdir -p {app_dir})&&" f"(test -d {data_dir}|| mkdir -p {data_dir})&&" f"(test -d {log_dir}|| mkdir -p {log_dir})&&" f"cd {app_dir} &&" f" tar -xmf {self.ntpdate_package_name} &&" f" /bin/rm -rf {self.ntpdate_package_name} &&" f"sed -i -e \"s#\${{CW_INSTALL_APP_DIR}}#{app_dir}#g\" -e 's#\${{CW_NTP_ADDRESS}}#{host_obj.ntpd_server}#g' {ntpdate_cron_path} &&" f"sed -i -e 's#\${{CW_INSTALL_APP_DIR}}#{app_dir}#g' -e 's#\${{CW_INSTALL_LOGS_DIR}}#{log_dir}#g' -e 's#\${{CW_INSTALL_DATA_DIR}}#{data_dir}#g' -e's#\${{CW_RUN_USER}}#{self.get_run_user()}#g' {scripts_path};" f"bash {scripts_path} start", timeout=120 ) logger.info(f"install ntpdate cmd, cmd_flag: {cmd_flag};cmd_msg: {cmd_msg}") if not cmd_flag: host_obj.ntpdate_install_status = 3 host_obj.save() return cmd_flag, cmd_msg host_obj.ntpdate_install_status = 0 host_obj.save() return True, "success" def install(self): return self.execute(host_obj_lst=self.host_obj_list, thread_name_prefix="install_", func=self._install_ntpdate) ================================================ FILE: omp_server/utils/plugin/monitor_agent.py ================================================ # -*- coding: utf-8 -*- # Project: monitor_agent # Author: jon.liu@yunzhihui.com # Create time: 2021-10-07 13:45 # IDE: PyCharm # Version: 1.0 # Introduction: """ 安装、卸载、更新monitor agent """ import json import os import time import random import logging from concurrent.futures import ThreadPoolExecutor from db_models.models import Host from omp_server.settings import PROJECT_DIR from promemonitor.prometheus_utils import PrometheusUtils from utils.plugin.salt_client import SaltClient logger = logging.getLogger("server") class MonitorAgentManager(object): """ 监控Agent的管理类 """ def __init__(self, host_objs=None): """ 初始化方法 :param host_objs: 主机对象组成的列表 """ if not host_objs or not isinstance(host_objs, list): raise TypeError("host_objs must be a list of host objs!") self.name = "omp_monitor_agent" self.host_objs = host_objs self.package_hub_dir = os.path.join(PROJECT_DIR, "package_hub") self.monitor_agent_package_name = "" self.check_monitor_agent_package() def check_monitor_agent_package(self): """ 检查monitor agent的源码包是否存在 :return: """ # 判断monitor agent源码包文件是否存在 for item in os.listdir(self.package_hub_dir): if item.startswith(self.name) and item.endswith("tar.gz"): self.monitor_agent_package_name = item break @staticmethod def execute(host_obj_lst, thread_name_prefix, func): """ 执行函数 :param host_obj_lst: 主机对象组成的列表 :param thread_name_prefix: 线程前缀 :param func: 执行函数对象, 仅接收host主机对象参数 :return: """ thread_p = ThreadPoolExecutor( max_workers=20, thread_name_prefix=thread_name_prefix) # futures_list:[(ip, future)] futures_list = list() for item in host_obj_lst: future = thread_p.submit(func, item) futures_list.append((item.ip, future)) # result_list:[(ip, res_bool, res_msg), ...] result_list = list() for f in futures_list: result_list.append((f[0], f[1].result()[0], f[1].result()[1])) thread_p.shutdown(wait=True) error_msg = "" for el in result_list: if not el[1]: error_msg += \ f"{el[0]}: (execute_flag: {el[1]}; execute_msg: {el[2]});" if error_msg: return False, error_msg return True, "success!" def _install(self, obj): """ 安装函数 :param obj: 主机对象 :type obj: Host :return: """ # step1: 判断源码包是否存在 if not self.monitor_agent_package_name: return False, "omp_monitor_agent package does not exist!" # step2: 发送源码包文件 salt_obj = SaltClient() send_flag, send_msg = salt_obj.cp_file( target=obj.ip, source_path=self.monitor_agent_package_name, target_path=obj.agent_dir, makedirs=False) logger.info( f"Send omp_monitor_agent, " f"send_flag: {send_flag}; send_msg: {send_msg}") if not send_flag: return send_flag, send_msg # step3: 解压源码包并启动服务 metrics_auth = json.dumps( {"username": "mokey", "password": "w7SiYs$oE"}) # TODO 后续从配置文件读取 services_info = json.dumps({"node": {"quotes": [], "exporter_port": 19017, "exporter_metric": "metrics", "auth_user": "mokey", "auth_password_encryption": "$2y$12$GThv30aK.STxvx6A32CXVubbveBkEq3vatYTPpuIVTT6uRE24L91O"}}) cmd_flag, cmd_res = salt_obj.cmd( target=obj.ip, command=f"cd {obj.agent_dir} && " f"tar -xmf {self.monitor_agent_package_name} && " f"/bin/rm -rf {self.monitor_agent_package_name} && " f"cd {self.name} && " f"./install --agent_ip={obj.ip} --metrics_auth='{metrics_auth}' --services_info='{services_info}' && " f"bash monitor_agent.sh init &&" f"bash monitor_agent.sh start", timeout=120) logger.info( f"Install omp_monitor_agent cmd, " f"cmd_flag: {cmd_flag}; cmd_res: {cmd_res}") # 安装成功后更新数据库的状态 if not cmd_flag: Host.objects.filter(ip=obj.ip).update( monitor_agent=4, monitor_agent_error=str(cmd_res) if len( str(cmd_res)) < 200 else str(cmd_res)[:200] ) return cmd_flag, cmd_res Host.objects.filter(ip=obj.ip).update( monitor_agent=0, monitor_agent_error=None ) return True, "success" def install(self): """ 安装monitor agent :return: """ flag, msg = self.execute( host_obj_lst=self.host_objs, thread_name_prefix="install_", func=self._install) if not flag: return flag, msg # 更新监控Server端配置,暂时靠sleep解决文件并发操作 time.sleep(random.randint(1, 5)) _pro_obj = PrometheusUtils() _pro_obj.add_node(self.parse_hosts_data()) return True, "success!" def parse_hosts_data(self): """ 解析主机信息 :return: """ hosts_data = list() for item in self.host_objs: # TODO 支持手动添加服务器,暂不支持无SSH _ip = item.ip _env = item.env.name _data_folder = item.data_folder # {"/": 90, "/data": 100} _disk_info = item.disk if not _disk_info: _disk_info = Host.objects.get(id=item.id).disk data_path = "" if _disk_info and isinstance(_disk_info, dict): for key, _ in _disk_info.items(): if key == "/": continue if _data_folder.startswith(key): data_path = key break # prometheus 使用的target数据 hosts_data.append({ "ip": _ip, "env": _env, "data_path": data_path or _data_folder, "instance_name": item.instance_name }) return hosts_data def _uninstall(self, obj): """ 卸载函数 :param obj: 主机对象 :type obj: Host :return: """ salt_obj = SaltClient() monitor_agent_home = os.path.join(obj.agent_dir, self.name) cmd_flag, cmd_res = salt_obj.cmd( target=obj.ip, command=f"cd {monitor_agent_home} && " f"./manage stop_all && bash monitor_agent.sh stop && " f"cd {obj.agent_dir} && /bin/rm -rf {self.name}", timeout=120) logger.info( f"Uninstall monitor_agent, " f"cmd_flag: {cmd_flag}; cmd_res: {cmd_res}") if not cmd_flag: return cmd_flag, cmd_res return True, "success" def uninstall(self): """ 卸载monitor agent :return: """ # TODO 需要确定exporter停止脚本使用参数情况 return self.execute( host_obj_lst=self.host_objs, thread_name_prefix="uninstall_", func=self._uninstall) ================================================ FILE: omp_server/utils/plugin/public_utils.py ================================================ # -*- coding: utf-8 -*- # Project: public_utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-09 19:55 # IDE: PyCharm # Version: 1.0 # Introduction: """ 文件md5值处理模块 """ import os import socket import hashlib import ipaddress import subprocess def local_cmd(command): """ 执行本地shell命令 :param command: 执行命令 :return: (stdout, stderr, ret_code) """ p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True ) stdout, stderr = p.communicate() _out, _err, _code = \ stdout.decode("utf8"), stderr.decode("utf8"), p.returncode return _out, _err, _code def get_file_md5(file_path): """ 获取文件的md5值 :param file_path: 文件路径 :return: """ if not os.path.exists(file_path): return False, "File not exists!" m = hashlib.md5() with open(file_path, 'rb') as f_obj: while True: data = f_obj.read(4096) if not data: break m.update(data) return True, m.hexdigest() def check_is_ip_address(value): """ 检查是否为ip地址 :param value: ip地址字符串 :return: """ try: ipaddress.ip_address(value) return True, value except ValueError: return False, "not valid ip address!" def check_ip_port(ip, port): """ 检查ip、port的联通性 :param ip: 地址 :param port: 端口 :return: """ if not check_is_ip_address(value=ip)[0]: return False, "ip address not correct" try: int_port = int(port) if int_port < 0 or int_port > 65535: return False, "port must be 0 ~ 65535" except ValueError: return False, "port must be 0 ~ 65535, int or string" sock_obj = socket.socket(socket.AF_INET, socket.SOCK_STREAM) result = sock_obj.connect_ex((ip, port)) if result == 0: return_tuple = (True, "success") else: return_tuple = (False, "failed") sock_obj.close() return return_tuple class DurationTime: def __init__(self, seconds): self.second = seconds self.minute = self.hour = self.day = 0 def analysis_day(self): day, hour = divmod(self.hour, 24) setattr(self, "hour", hour) setattr(self, "day", day) def analysis_hour(self): hour, minute = divmod(self.minute, 60) setattr(self, "minute", minute) setattr(self, "hour", hour) def analysis_minute(self): minute, second = divmod(self.second, 60) setattr(self, "second", second) setattr(self, "minute", minute) def __call__(self, *args, **kwargs): for key in ["minute", "hour", "day"]: getattr(self, f"analysis_{key}")() if not getattr(self, key): return self return self def timedelta_strftime(timedelta): """ 四舍五入格式化timedelta :param timedelta: :return: "XX天XX时XX分XX秒" """ seconds = round(timedelta.total_seconds()) duration = DurationTime(seconds)() en_zh = [("day", "天"), ("hour", "时"), ("minute", "分"), ("second", "秒")] strftime = "" for en, zh in en_zh: if strftime: strftime += f"{getattr(duration, en)}{zh}" elif not strftime and getattr(duration, en): strftime += f"{getattr(duration, en)}{zh}" return strftime def file_md5(file_path): # md5校验生成 md5_out = local_cmd(f'md5sum {file_path}') if md5_out[2] != 0: return None return md5_out[0].split()[0] def format_location_size(size): # 格式化文件大小 if int(size/1024) < 100: return "%.3f" % (size / 1024) + "K" size = size/1024 if int(size/1024) < 100: return "%.3f" % (size / 1024) + "M" return "%.3f" % (size/1024/1024) + "G" ================================================ FILE: omp_server/utils/plugin/salt_client.py ================================================ # -*- coding: utf-8 -*- # Project: salt_client # Author: jon.liu@yunzhihui.com # Create time: 2020-12-31 14:46 # IDE: PyCharm # Version: 1.0 # Introduction: 定义与salt交互过程中使用的某些类及方法。 """ salt 封装的相关模块或方法 """ import os import logging import traceback import salt.client from omp_server.settings import PROJECT_DIR salt_master_config = os.path.join(PROJECT_DIR, "config/salt/master") logger = logging.getLogger('server') SALT_ERROR_MSG = "Salt 执行错误,请检查相关配置是否正确!" AGENT_OFFLINE_MSG = "当前目标主机不在线或该目标主机未纳管!" class SaltClient(object): """本地salt管理接口""" def __init__(self, config_path=salt_master_config): """ salt管理客户端初始化操作 :param config_path: salt-master的配置文件绝对路径 """ self.config_path = config_path # logger.info( # f"Init salt client with config path: {self.config_path}!") self.client = salt.client.LocalClient( c_path=self.config_path, auto_reconnect=True) def salt_module_update(self): """ 用于更新salt的自定义模块 :return: """ try: cmd_res = self.client.cmd( tgt="*", fun="saltutil.sync_modules", ) """ # {'192.168.175.149': {'ret': [], 'retcode': 0, 'jid': '20210113213356939481'}, 'ruban-dev': False} # {'192.168.175.149': [], 'ruban-dev': False} """ ret_dic = dict() for key, value in cmd_res.items(): if isinstance(value, dict) and value.get("retcode", 1000) == 0: ret_dic.update({key: True}) elif isinstance(value, list): ret_dic.update({key: True}) else: ret_dic.update({key: False}) return True, ret_dic except Exception as e: return False, f"在同步salt模块的过程中出错: {str(e)}" def fun_for_multi(self, target, fun, arg=(), kwarg=None, timeout=None, tgt_type="glob"): """ 可自行执行模块的命令,适用于批量执行操作,需要自行判断函数执行结果 :param target: 目标主机 :param fun: 执行模块,如cmd.run :param arg: 位置参数信息 :param kwarg: 关键字参数信息 :param timeout: 超时时间 :param tgt_type: 匹配target的格式,glob正则匹配 or list匹配 :return: 返回的执行结果 """ try: logger.info( f"Execute by salt fun_for_multi: {target}|{fun}|{arg}|{kwarg}|{timeout}") if kwarg is None: kwarg = {} cmd_res = self.client.cmd( tgt=target, fun=fun, arg=arg, kwarg=kwarg, tgt_type=tgt_type, timeout=timeout, full_return=True ) logger.info(f"Execute by salt fun_for_multi res: {cmd_res}!") return cmd_res except Exception as e: logger.error( f"Execute by salt fun_for_multi with Exception: {traceback.format_exc()}") return False, f"执行{str(fun)}过程中出现错误: {str(e)}" def fun(self, target, fun, arg=(), kwarg=None, timeout=None): """ 可自行执行模块的命令,适用于单个主机的部分模块 :param target: 目标主机 :param fun: 执行模块,如cmd.run :param arg: 位置参数信息 :param kwarg: 关键字参数信息 :param timeout: 超时时间 :return: 返回的执行结果 """ try: logger.info( f"Execute by salt fun: {target}|{fun}|{arg}|{kwarg}|{timeout}") if kwarg is None: kwarg = {} cmd_res = self.client.cmd( tgt=target, fun=fun, arg=arg, kwarg=kwarg, timeout=timeout, full_return=True ) logger.info(f"Execute by salt fun res: {cmd_res}!") if not isinstance(cmd_res, dict): return False, SALT_ERROR_MSG if target not in cmd_res: return False, AGENT_OFFLINE_MSG if target in cmd_res and cmd_res[target] is False: return False, "当前主机agent状态异常!" if 'retcode' not in cmd_res[target]: return False, f"当前执行未出现预期结果,详情如下: {cmd_res[target]}" if cmd_res[target]["retcode"] != 0: return False, cmd_res[target]["ret"] return True, cmd_res[target]["ret"] except Exception as e: logger.error( f"Execute by salt fun_for_multi with Exception: {traceback.format_exc()}") return False, f"执行{str(fun)}过程中出现错误: {str(e)}" def cmd(self, target, command, timeout, real_timeout=None): """ 执行shell命令接口 :param target: 目标agent的id,一般为ip :param command: 将要执行的shell命令 :param timeout: salt连接超时时间 :param real_timeout: cmd命令执行超时时间 :return: 命令执行结果 """ try: logger.info( f"Execute by salt cmd: {target}|{command}|{timeout}") cmd_res = self.client.cmd( tgt=target, fun="cmd.run", arg=(command,), timeout=timeout, full_return=True, kwarg={"timeout": real_timeout} ) logger.info(f"Execute by salt cmd res: {cmd_res}") if not isinstance(cmd_res, dict): return False, SALT_ERROR_MSG if target not in cmd_res: return False, AGENT_OFFLINE_MSG if cmd_res[target] is False: return False, AGENT_OFFLINE_MSG if 'retcode' not in cmd_res[target]: return False, f"当前执行未出现预期结果,详情如下: {cmd_res[target]}" if cmd_res[target]["retcode"] != 0: return False, cmd_res[target]["ret"] if "Timed out after" in cmd_res[target]["ret"]: return False, cmd_res[target]["ret"] return True, cmd_res[target]["ret"] except Exception as e: logger.error(f"Execute by salt cmd with Exception: {str(e)}") return False, f"执行命令的过程中出现错误: {str(e)}" def cp_file(self, target, source_path, target_path, makedirs=True): """ salt-master发送文件到目标服务器 :param target: 目标主机 :param source_path: 源文件路径,salt:// :param target_path: 目标主机上存放路径 :param makedirs: 当文件夹不存在时,是否在目标主机上创建文件夹 :return: """ try: logger.info( f"Execute by salt cp_file: {target}|{source_path}|{target_path}|{makedirs}") source_path = "salt://" + source_path cmd_res = self.client.cmd( tgt=target, fun="cp.get_file", arg=(source_path, target_path), kwarg={"makedirs": makedirs}, timeout=60 * 10 ) logger.info(f"Execute by salt cp_file res: {cmd_res}") if not isinstance(cmd_res, dict): return False, SALT_ERROR_MSG if target not in cmd_res: return False, AGENT_OFFLINE_MSG if "PermissionError" in cmd_res[target] and "Permission denied" in cmd_res[target]: return False, "当前目标主机上此用户无法在目标路径下创建目录或文件!" if str(cmd_res[target]).startswith(target_path) or \ not cmd_res[target]: return True, cmd_res[target] return False, f"当前出现未知错误: {cmd_res[target]}" except Exception as e: logger.error( f"Execute by salt cp_file with Exception: {traceback.format_exc()}") return False, f"发送文件过程中出现错误: {str(e)}" def cp_push(self, target, source_path, upload_path): """ salt-master从目标服务器拉取文件 拉取过来的文件存放路径为:/data/omp/data/salt/var/cache/salt/master/minions/10.0.3.24/files :param target: 目标主机 :param source_path: 目标主机上源文件路径,/data/backup :param upload_path: 目标主机上文件名 :return: """ try: cmd_res = self.client.cmd( tgt=target, fun="cp.push", arg=(source_path,), kwarg={"upload_path": upload_path, "remove_source": True}, timeout=60 * 10 ) logger.info(f"执行拉取文件的接口,获取到的返回结果是: {cmd_res}") if not isinstance(cmd_res, dict): return False, "Salt 执行错误,请检查相关配置是否正确!" if target not in cmd_res: return False, "当前目标主机不在线或该目标主机未纳管!" if cmd_res[target] is True: return True, "success" return False, f"当前出现未知错误: {cmd_res[target]}" except Exception as e: print(traceback.format_exc()) logger.error(f"拉取文件过程中程序出现错误: {traceback.format_exc()}") return False, f"拉取文件过程中出现错误: {str(e)}" ================================================ FILE: omp_server/utils/plugin/send_email.py ================================================ import logging import threading import traceback from django.core.mail import EmailMultiAlternatives from django.core.mail.backends.smtp import EmailBackend from db_models.models import EmailSMTPSetting logger = logging.getLogger("server") class ResultThread(threading.Thread): """ 需要运行结果的多线程 """ def __init__(self, target, args, name=''): threading.Thread.__init__(self) self.target = target self.name = name self.args = args def run(self): self.result = self.target(*self.args) def get_result(self): return self.result class ModelSettingEmailBackend: # 邮件 SMTP 服务器链接backend(修改默认的settings) @property def load_settings(self): if hasattr(self, "setting_kwargs"): return self.setting_kwargs setting_obj = EmailSMTPSetting.objects.first() setting_kwargs = {} if setting_obj: setting_kwargs = setting_obj.get_dict() setattr(self, "setting_kwargs", setting_obj.get_dict()) return setting_kwargs def load_connection(self): setting_kwargs = self.load_settings if not setting_kwargs: return None return EmailBackend(**setting_kwargs) class SendEmailContent: # 邮件内容基类 subject = "" # 邮件主题 from_user = "运维管理平台<{}>" # 发件人 is_html = False # 是否是html邮件 file = False # 存在的文件使用 file_content = False # 临时文件使用 def __init__(self, _obj): self._obj = _obj @property def fetch_content(self): # 获取邮件内容 if hasattr(self, "email_content"): return self.email_content email_content = self._obj.send_email_content() setattr(self, "email_content", email_content) return email_content def fetch_html_content(self): # 获取邮件html内容 pass def fetch_file_kwargs(self): # 文件 if hasattr(self, "file_kwargs"): return self.file_kwargs file_kwargs = self._obj.fetch_file_kwargs() setattr(self, "file_kwargs", file_kwargs) return file_kwargs def fetch_file_content(self): if hasattr(self, "file_content"): return self.file_content file_content = self._obj.fetch_file_content() setattr(self, "file_content", file_content) return file_content class SendAlertEmailContent(SendEmailContent): subject = "运维管理平台警消息通知" class SendBackupHistoryEmailContent(SendEmailContent): subject = "运维管理平台备份通知邮件" file = True class SendDeepInspectionEmailContent(SendBackupHistoryEmailContent): subject = "运维管理平台深度巡检报告通知邮件" file = True class SendNormalInspectionEmailContent(SendBackupHistoryEmailContent): subject = "运维管理平台主机、组件巡检报告通知邮件" file = True class EmailSendTool(object): # 邮件发送 def __init__(self, con_backend, content, users): from_user = content.from_user.format( con_backend.load_settings.get("username")) self.email_msg = EmailMultiAlternatives( content.subject, content.fetch_content, from_user, users, connection=con_backend.load_connection() ) self.content = content self.need_send_count = len(users) def send(self): """ 发送邮件 :return: """ try: if self.content.is_html: self.email_msg.attach_alternative( self.content.fetch_html_content, "text/html") if self.content.file: self.email_msg.attach_file( **self.content.fetch_file_kwargs() ) elif self.content.file_content: self.email_msg.attach( **self.content.fetch_file_content() ) except Exception as e: logger.error(f"发送邮件失败,失败原因: {str(e)},详情为:{traceback.format_exc()}") try: count = self.email_msg.send() except Exception as e: logger.error(f"发送邮件失败,失败原因:{str(e)}") return False if self.need_send_count - count: return False return True def many_send(connection, content, users): """ 多个并发同时发送,用以区分失败人 :param connection: 邮件服务器配置对象:ModelSettingEmailBackend :param content: 邮件内容对象:SendEmailContent :param users: 用户邮箱list、set :return: """ threading_list = [] for user in users: send_tool = EmailSendTool(connection, content, (user,)) _thread = ResultThread( target=send_tool.send, args=()) threading_list.append(_thread) threading_list_result = [] [thread_obj.start() for thread_obj in threading_list] for thread_obj in threading_list: thread_obj.join() threading_list_result.append(thread_obj.get_result()) fail_user = [] for user, result in zip(users, threading_list_result): if not result: fail_user.append(user) return fail_user ================================================ FILE: omp_server/utils/plugin/ssh.py ================================================ """ ssh相关操作 """ import os import logging import paramiko from scp import SCPClient from utils.parse_config import ( SSH_CMD_TIMEOUT, SSH_CHECK_TIMEOUT ) logger = logging.getLogger("server") class SSH(object): """ SSH 工具类 """ def __init__(self, hostname, port, username, password, timeout=SSH_CHECK_TIMEOUT): """ 初始化ssh :param hostname: 主机名或ip地址 :param port: 端口 :param username: 用户名 :param password: 密码 :param timeout: 超时时间 """ logger.info( f"SSH init with params: {hostname}; {port}; {username}; {timeout}") self.hostname = hostname self.port = port self.username = username self.password = password self.timeout = timeout # 连接对象 self.connect = None self.ssh_client = None self.scp_client = None # 错误信息 self.is_error = None self.error_message = None def _get_connection(self): """ 获取连接对象 """ if not self.connect: try: ssh_client = paramiko.SSHClient() ssh_client.set_missing_host_key_policy( paramiko.AutoAddPolicy()) ssh_client.connect( hostname=self.hostname, port=self.port, username=self.username, password=self.password, timeout=self.timeout ) scp_client = SCPClient(ssh_client.get_transport()) except Exception as error: self.is_error = True self.error_message = error self.close() return self.ssh_client = ssh_client self.scp_client = scp_client def check(self): """ 检查SSH连接信息 :return: is_connect, message """ self._get_connection() if self.is_error: return False, str(self.error_message) _, stdout, _ = self.ssh_client.exec_command("whoami") who = stdout.readline().strip() if who == self.username: return True, "check passed" return False, f"stdout: {who}" def is_sudo(self): """ 检查用户是否具有sudo权限 :return: is_sudo, message """ self._get_connection() if self.is_error: return False, str(self.error_message) _, stdout, _ = self.ssh_client.exec_command( "sudo -n echo 'success'", get_pty=True) res = stdout.readline().strip() if res == "success": return True, "is sudo" return False, "not sudo" def cmd(self, command, timeout=SSH_CMD_TIMEOUT, get_pty=True): """ 执行shell命令 :param command: :param timeout: :param get_pty: :return: """ self._get_connection() if self.is_error: return False, str(self.error_message) _, stdout, stderr = self.ssh_client.exec_command( command, get_pty=get_pty, timeout=timeout) stdout.channel.recv_exit_status() res_stdout = stdout.readlines() res_stderr = stderr.readlines() if len(res_stderr) != 0: return False, res_stderr[0].strip() + " " + str(stdout) return True, "\n".join(res_stdout) def make_remote_path_exist(self, remote_path): """ mkdir -p remote_path :param remote_path: 远程文件夹路径 :type remote_path str :return: """ self._get_connection() if self.username == "root": command = "test -d {0} || mkdir -p {0}".format(remote_path) else: is_sudo_flag, _ = self.is_sudo() if is_sudo_flag: command = f"sudo mkdir -p {remote_path} && " \ f"sudo chown -R {self.username}.{self.username} {remote_path}" else: # 普通用户仅尝试创建文件夹 command = "test -d {0} || mkdir -p {0}".format(remote_path) self.cmd(command) def file_push(self, file, remote_path="/tmp"): """ push file to remote directory use scp :param str file: file path :param str remote_path: remote directory path """ file_name = os.path.basename(file) remote_file_full_path = os.path.join(remote_path, file_name) self._get_connection() if self.is_error: return False, str(self.error_message) try: self.make_remote_path_exist(remote_path) self.scp_client.put(file, recursive=True, remote_path=remote_file_full_path) except Exception as error: import traceback logger.error(traceback.format_exc()) self.close() return False, str(error) return True, "push success: {}".format(remote_file_full_path) def close(self): """ 关闭连接对象 """ if self.ssh_client: self.ssh_client.close() if self.scp_client: self.scp_client.close() ================================================ FILE: omp_server/utils/plugin/synch_grafana.py ================================================ import json import time import traceback import requests from db_models.models import MonitorUrl, GrafanaMainPage from utils.parse_config import GRAFANA_AUTH def make_request(url, headers, payload): """ 请求 :param url: :param headers: :param payload: :return: """ flag = 0 auth = (GRAFANA_AUTH.get("grafana_admin_auth").get("username", "admin"), GRAFANA_AUTH.get("grafana_admin_auth").get("plaintext_password", "Yunweiguanli@OMP_123")) while flag < 5: response = requests.get(url=url, headers=headers, data=payload, auth=auth) r = json.loads(response.text) for url in r: if not isinstance(url, dict): break else: return True, r flag += 1 time.sleep(30) return False, None def synch_grafana_info(): """如果存在则不再添加,修改会追加一条数据""" monitor_ip = MonitorUrl.objects.filter(name="grafana") monitor_url = monitor_ip[0].monitor_url if len( monitor_ip) else "127.0.0.1:19014" url = "http://{0}/proxy/v1/grafana/api/search?" \ "query=&starred=false&skipRecent=false&skipStarred=false&" \ "folderIds=0&layout=folders".format(monitor_url) payload = {} headers = {'Content-Type': 'application/json'} try_times = 0 while try_times <= 3: try: try_times += 1 # print(f"start request to: {url}") flag, r = make_request(url=url, headers=headers, payload=payload) if not flag: return break except requests.exceptions.MissingSchema as e: print(f"grafana error: {str(e)}, try again after 10s!") except Exception as e: print(e) print(traceback.format_exc()) return else: return url_type = {"service": "fu", "node": "zhu", "log": "applogs"} url_dict = {} for url in r: url_name = url.get("uri").rsplit("/", 1)[1].split("-", 1)[0] url_dict[url_name.lower()] = url.get("url") for key, value in url_type.items(): url_dict.update({key: url_dict.pop(value)}) if GrafanaMainPage.objects.all().count() != len(url_dict): dbname = [i.instance_name for i in GrafanaMainPage.objects.all()] difference = list(set(url_dict.keys()).difference(set(dbname))) grafana_obj = [GrafanaMainPage( instance_name=i, instance_url=url_dict.get(i)) for i in difference] GrafanaMainPage.objects.bulk_create(grafana_obj) ================================================ FILE: omp_server/utils/prometheus/__init__.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/14 4:49 下午 # Description: ================================================ FILE: omp_server/utils/prometheus/create_html_tar.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/26 9:45 上午 # Description: import os import json import subprocess from django.conf import settings def cmd(command): """执行本地shell命令""" p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True) stdout, stderr = p.communicate() _out, _err, _code = \ stdout.decode("utf8"), stderr.decode("utf8"), p.returncode return _out, _err, _code def create_html_tar(new_html_dir_name, report_data): """ 生成巡检报告tar.gz包 :param new_html_dir_name: 包名 :param report_data: 报告数据 :return: """ # 拷贝目录 inspection_file = os.path.join( settings.PROJECT_DIR, f"data/inspection_file/{new_html_dir_name}") old_file_dir = os.path.join( settings.PROJECT_DIR, "package_hub/template/inspection_html") _out, _err, _code = cmd(f"cp -r {old_file_dir} {inspection_file}") if _code: return False, "拷贝文件失败!" # 修改源数据 js_path = os.path.join(inspection_file, "static/js/main.e4ade54a.chunk.js") with open(js_path, "r") as f: content = f.read() content = content.replace("'!@#$'", json.dumps(report_data)) with open(js_path, "w") as f: f.write(content) # 打包 inspection_dir = os.path.join(settings.PROJECT_DIR, f"data/inspection_file") tar_file = f"{new_html_dir_name}.tar.gz" _out, _err, _code = cmd( f"cd {inspection_dir} && tar -cvf {tar_file} {new_html_dir_name}") # 删除源文件 cmd(f"rm -rf {inspection_file}") if _code: return False, "打包巡检报告失败" return True, tar_file ================================================ FILE: omp_server/utils/prometheus/prometheus.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/14 4:01 下午 # Description: import json import logging import requests from datetime import datetime from db_models.models import MonitorUrl from db_models.models import InspectionHistory, InspectionReport from utils.parse_config import PROMETHEUS_AUTH logger = logging.getLogger("server") class Prometheus(object): """ prometheus 执行prosql查询数据、查询alerts """ def __init__(self): # prometheus 的 ip:port self.address = MonitorUrl.objects.get(name='prometheus').monitor_url self.basic_auth = (PROMETHEUS_AUTH.get( "username", "omp"), PROMETHEUS_AUTH.get("plaintext_password", "")) def query(self, expr): """ 请求prometheus开放接口,执行prosql,查询数据 :para expr: 需要执行的sql :return: 查询到的实时数据 """ url = f"http://{self.address}/api/v1/query?query={expr}" try: rsp = json.loads(requests.get(url=url, timeout=0.5, auth=self.basic_auth ).content.decode('utf8', 'ignore')) if rsp.get('status') == 'success': return True, rsp.get('data') else: return False, {} except Exception as e: logger.error(f"Query query from prometheus error: {str(e)}") return False, {} @staticmethod def clean_alert(alerts): """ 清洗告警,去掉同类不同级别告警 :param alerts: :return: """ unique_alert_dic = dict() clean_alerts = list() for item in alerts: _key = item.get("labels", {}).get("alertname", "") + \ item.get("labels", {}).get("instance", "") _level = item.get("labels", {}).get("severity", "") if _key not in unique_alert_dic: unique_alert_dic[_key] = { "warning": "", "critical": "" } unique_alert_dic[_key][_level] = item for key, value in unique_alert_dic.items(): if value.get("critical"): clean_alerts.append(value.get("critical")) else: clean_alerts.append(value.get("warning")) return clean_alerts @staticmethod def unified_job(is_success, ret): """ 实例方法 返回值统一处理 :ret: 返回值 :is_success: 请求是否成功 """ if is_success: if ret.get('result'): return ret['result'][0].get('value')[1] else: return 0 else: return 0 def query_alerts(self): url = f'http://{self.address}/api/v1/alerts' try: rsp = json.loads(requests.get(url=url, timeout=0.5, auth=self.basic_auth ).content.decode('utf8', 'ignore')) if rsp.get('status') == 'success': # 处理重复级别告警问题 jon.liu alerts = rsp.get('data').get('alerts') return self.clean_alert(alerts) else: return {} except Exception as e: logger.error(f"Query Alerts from prometheus error: {str(e)}") return {} def back_fill(history_id, report_id, host_data=None, serv_data=None, serv_plan=None, risk_data=None, scan_info=None, scan_result=None, file_name=None): """ 异步反填报告数据 :history_id : 巡检历史记录id :report_id : 巡检报告id :host_data : 主机巡检数据 :serv_data : 组件巡检数据 :serv_plan : 服务 :risk_data : 报警数据 :scan_info : 扫描统计 :scan_result : 分析结果 :file_name : 导出文件名 """ # 反填巡检历史记录InspectionHistory表,结束时间end_time、巡检用时duration字段、巡检状态inspection_status now = datetime.now() his_obj = InspectionHistory.objects.filter(id=history_id) duration = (now - his_obj[0].start_time).seconds duration = duration if duration > 0 else 1 his_obj.update(end_time=now, duration=duration, inspection_status=2) # 反填巡检报告InspectionReport表 rep_obj = InspectionReport.objects.filter(id=report_id) if host_data: # 反填巡检报告InspectionReport表,主机列表host_data字段 rep_obj.update(host_data=host_data) if serv_data: # 反填巡检报告InspectionReport表,服务列表serv_data字段 rep_obj.update(serv_data=serv_data) if serv_plan: # 反填巡检报告InspectionReport表,服务列表serv_plan字段 rep_obj.update(serv_plan=serv_plan) if risk_data: # 反填巡检报告InspectionReport表,服务列表risk_data字段 rep_obj.update(risk_data=risk_data) # 反填巡检报告InspectionReport表scan_info、scan_result rep_obj.update( scan_info=scan_info, scan_result=scan_result, file_name=file_name) ================================================ FILE: omp_server/utils/prometheus/target_host.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/14 6:26 下午 # Description: 主机指标 import json import logging import random from datetime import datetime from db_models.models import Host from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus from utils.prometheus.utils import get_host_data_folder logger = logging.getLogger("server") def target_host_thread(env, instance): """ 主机巡检,多线程执行 :env: 环境 queryset 对象 :instance: 主机ip地址 """ temp = dict() # 主机 prometheus 数据请求 h_w_obj = HostCrawl(env=env.name, instance=instance) h_w_obj.run() _p = h_w_obj.ret temp['id'] = random.randint(1, 99999999) temp['mem_usage'] = _p.get('mem_usage') temp['cpu_usage'] = _p.get('cpu_usage') temp['disk_usage_root'] = _p.get('disk_usage_root') temp['disk_usage_data'] = _p.get('disk_usage_data') temp['sys_load'] = _p.get('sys_load') temp['run_time'] = _p.get('run_time') temp['host_ip'] = instance temp['memory_top'] = _p.get('memory_top', []) temp['cpu_top'] = _p.get('cpu_top', []) temp['kernel_parameters'] = _p.get('kernel_parameters', []) # 操作系统 _h = Host.objects.filter(ip=instance).first() temp['release_version'] = _h.operate_system if _h else '' # 配置信息 host_massage = \ f"{_h.cpu if _h else '-'}C|{_h.memory if _h else '-'}G|" \ f"{sum(_h.disk.values()) if _h and _h.disk else '-'}G" temp['host_massage'] = host_massage temp['basic'] = [ {"name": "IP", "name_cn": "主机IP", "value": instance}, {"name": "hostname", "name_cn": "主机名", "value": _h.host_name if _h else '-'}, {"name": "kernel_version", "name_cn": "内核版本", "value": _p.get('kernel_version')}, {"name": "selinux", "name_cn": "SElinux 状态", "value": _p.get('selinux')}, {"name": "max_openfile", "name_cn": "最大打开文件数", "value": _p.get('max_openfile')}, {"name": "iowait", "name_cn": "IOWait", "value": _p.get('iowait')}, {"name": "inode_usage", "name_cn": "inode 使用率", "value": {"/": _p.get('inode_usage')}}, {"name": "now_time", "name_cn": "当前时间", "value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")}, {"name": "run_process", "name_cn": "进程数", "value": _p.get('run_process')}, {"name": "umask", "name_cn": "umask", "value": _p.get('umask')}, {"name": "bandwidth", "name_cn": "带宽", "value": _p.get('bandwidth')}, {"name": "throughput", "name_cn": "IO", "value": _p.get('throughput')}, {"name": "zombies_process", "name_cn": "僵尸进程", "value": _p.get('zombies_process')} ] return temp class HostCrawl(Prometheus): """ 查询 prometheus 主机指标 """ def __init__(self, env, instance): self.ret = {} self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() Prometheus.__init__(self) @staticmethod def unified_job(is_success, ret): """ 实例方法 返回值统一处理 :ret: 返回值 :is_success: 请求是否成功 """ if is_success: if ret.get('result'): return ret['result'][0].get('value')[1] else: return 0 else: return 0 def run_status(self): """运行状态""" expr = f"round(up{{env='{self.env}', instance='{self.instance}', " \ f"job='nodeExporter'}})" self.ret['run_status'] = self.unified_job(*self.query(expr)) def run_time(self): """运行时间""" expr = f"avg(time() - node_boot_time_seconds{{env='{self.env}'," \ f"instance=~'{self.instance}'}})" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """总cpu使用率""" expr = f"100 - (avg(rate(node_cpu_seconds_total" \ f"{{env='{self.env}'," \ f"instance=~'{self.instance}'," \ f"mode='idle'}}[5m])) * 100)" self.ret['cpu_usage'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}%" def iowait(self): """IO wait使用率""" expr = f"avg(rate(node_cpu_seconds_total{{env='{self.env}'," \ f"instance=~'{self.instance}',mode='iowait'}}[5m])) * 100" self.ret['iowait'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}%" def mem_usage(self): """内存使用率""" expr = f"(1 - (node_memory_MemAvailable_bytes" \ f"{{env='{self.env}'," \ f"instance=~'{self.instance}'}} / " \ f"(node_memory_MemTotal_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'}})))* 100" self.ret['mem_usage'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}%" def disk_usage_root(self): """根分区使用率""" expr = f"(node_filesystem_size_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'," \ f"fstype=~'ext.*|xfs',mountpoint='/'}} - " \ f"node_filesystem_avail_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'," \ f"fstype=~'ext.*|xfs',mountpoint='/'}}) / " \ f"node_filesystem_size_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'," \ f"fstype=~'ext.*|xfs',mountpoint='/'}} * 100" self.ret['disk_usage_root'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}%" def disk_usage_data(self): """数据分区使用率""" # 数据分区应该由主机表中的data_folder目录决定 # 并协同disk信息判断出数据分区挂载点是哪个 _data_path = get_host_data_folder(self.instance) if not _data_path: return "_" expr = f"(1-(node_filesystem_free_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}',mountpoint='{_data_path}', " \ f"fstype=~'ext.*|xfs'}}" \ f" / node_filesystem_size_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}',mountpoint='{_data_path}', " \ f"fstype=~'ext.*|xfs'}}))" \ f" * 100" _ = self.unified_job(*self.query(expr)) self.ret['disk_usage_data'] = f"{round(float(_), 2)}%" if _ else '_' def rate_exchange_disk(self): """交换分区使用率""" expr = f"(1 - (node_memory_SwapFree_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'}} / " \ f"node_memory_SwapTotal_bytes{{env='{self.env}'," \ f"instance=~'{self.instance}'}})) * 100" self.ret['rate_exchange_disk'] = self.unified_job(*self.query(expr)) def rate_cpu_io_wait(self): """cpu io wait""" expr = f"avg(rate(node_cpu_seconds_total" \ f"{{instance=~'{self.instance}',mode='iowait'}}[5m])) * 100" self.ret['rate_cpu_io_wait'] = self.unified_job(*self.query(expr)) def inode_usage(self): """inode使用率""" expr = f"(1-node_filesystem_files_free{{fstype=~'xfs|ext4'," \ f"mountpoint='/', env='{self.env}'," \ f"instance=~'{self.instance}'}} / " \ f"node_filesystem_files{{fstype=~'xfs|ext4',mountpoint='/', " \ f"env='{self.env}',instance=~'{self.instance}'}})*100" self.ret['inode_usage'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}%" def max_openfile(self): """总文件描述符""" expr = f"avg(node_filefd_maximum{{instance=~'{self.instance}'}})" self.ret['max_openfile'] = self.unified_job(*self.query(expr)) def sys_load(self): """系统平均负载""" # 1分钟负载 expr = f"node_load1{{env='{self.env}',instance=~'{self.instance}'}}" load1 = self.unified_job(*self.query(expr)) # 5分钟负载 expr = f"node_load5{{env='{self.env}',instance=~'{self.instance}'}}" load5 = self.unified_job(*self.query(expr)) # 15分钟负载 expr = f"node_load15{{env='{self.env}',instance=~'{self.instance}'}}" load15 = self.unified_job(*self.query(expr)) self.ret['sys_load'] = f"{load1},{load5},{load15}" def bandwidth(self): """网络带宽使用""" # eth0 下载 expr = f"sum(rate(node_network_receive_bytes_total{{env='{self.env}'," \ f"instance=~'{self.instance}',device=~'eth0'}}[2m])*8/1024)" self.ret['bandwidth'] = { 'receive': f"{round(float(self.unified_job(*self.query(expr))), 2)}" f"kb/s"} # eth0 上传 expr = f"sum(rate(node_network_transmit_bytes_total" \ f"{{env='{self.env}',instance=~'{self.instance}'," \ f"device=~'eth0'}}[2m])*8/1024)" self.ret['bandwidth']['transmit'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}kb/s" def throughput(self): """磁盘io""" # 读 expr = f"sum(rate(node_disk_read_bytes_total{{env='{self.env}'," \ f"instance=~'{self.instance}'}}[2m])) / 1024" self.ret['throughput'] = \ {'read': f"{round(float(self.unified_job(*self.query(expr))), 2)}" f"kb/s"} # 写 expr = f"sum(rate(node_disk_written_bytes_total" \ f"{{env='{self.env}'," \ f"instance=~'{self.instance}'}}[2m])) / 1024" self.ret['throughput']['write'] = \ f"{round(float(self.unified_job(*self.query(expr))), 2)}kb/s" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "host_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception as e: logger.error(f"Salt host_check.main failed with error: {str(e)}") ret = {} self.ret['memory_top'] = ret.get('memory_top', []) self.ret['cpu_top'] = ret.get('cpu_top', []) self.ret['kernel_parameters'] = ret.get('kernel_parameters', []) self.ret['kernel_version'] = ret.get('kernel_version') self.ret['selinux'] = ret.get('selinux') self.ret['run_process'] = ret.get('run_process') self.ret['umask'] = ret.get('umask') self.ret['zombies_process'] = ret.get('zombies_process') def run(self): """统一执行实例方法""" # target为实例方法,目的是统一执行实例方法并统一返回值 target = ['mem_usage', 'cpu_usage', 'disk_usage_root', 'disk_usage_data', 'sys_load', 'run_time', 'max_openfile', 'inode_usage', 'iowait', 'bandwidth', 'throughput', 'salt_json', 'run_status'] for t in target: if getattr(self, t): getattr(self, t)() if __name__ == '__main__': h = HostCrawl(env='default', instance='10.0.14.224') h.run() print(h.ret) ================================================ FILE: omp_server/utils/prometheus/target_service.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/22 10:04 下午 # Description: import json import random import logging import traceback from db_models.models import Service from utils.prometheus.thread import MyThread from utils.prometheus.target_service_jvm_base import ServiceBase from utils.prometheus.target_service_arangodb import ServiceArangodbCrawl from utils.prometheus.target_service_beanstalk import ServiceBeanstalkCrawl from utils.prometheus.target_service_clickhouse import ServiceClickhouseCrawl from utils.prometheus.target_service_elasticsearch import ServiceElasticsearchCrawl from utils.prometheus.target_service_flink import ServiceFlinkCrawl from utils.prometheus.target_service_gotty import ServiceGottyCrawl from utils.prometheus.target_service_grafana import ServiceGrafanaCrawl from utils.prometheus.target_service_hadoop import ServiceHadoopCrawl from utils.prometheus.target_service_httpd import ServiceHttpdCrawl from utils.prometheus.target_service_ignite import ServiceIgniteCrawl from utils.prometheus.target_service_ntpd import ServiceNtpdCrawl from utils.prometheus.target_service_postgresql import ServicePostgresqlCrawl from utils.prometheus.target_service_prometheus import ServicePrometheusCrawl from utils.prometheus.target_service_tengine import ServiceTengineCrawl from utils.prometheus.target_service_mysql import ServiceMysqlCrawl from utils.prometheus.target_service_redis import ServiceRedisCrawl from utils.prometheus.target_service_kafka import ServiceKafkaCrawl from utils.prometheus.target_service_nacos import ServiceNacosCrawl from utils.prometheus.target_service_rocketmq import ServiceRocketmqCrawl from utils.prometheus.target_service_zookeeper import ServiceZookeeperCrawl logger = logging.getLogger("server") open_source_class_dict = { "arangodb": ServiceArangodbCrawl, "beanstalk": ServiceBeanstalkCrawl, "clickhouse": ServiceClickhouseCrawl, "elasticsearch": ServiceElasticsearchCrawl, "flink": ServiceFlinkCrawl, "gotty": ServiceGottyCrawl, "grafana": ServiceGrafanaCrawl, "hadoop": ServiceHadoopCrawl, "httpd": ServiceHttpdCrawl, "ignite": ServiceIgniteCrawl, "kafka": ServiceKafkaCrawl, "mysql": ServiceMysqlCrawl, "nacos": ServiceNacosCrawl, "ntpd": ServiceNtpdCrawl, "postgreSql": ServicePostgresqlCrawl, "prometheus": ServicePrometheusCrawl, "redis": ServiceRedisCrawl, "rocketmq": ServiceRocketmqCrawl, "tengine": ServiceTengineCrawl, "zookeeper": ServiceZookeeperCrawl } def get_port_and_status(i): """ 组建巡检:统一获取每个服务的端口信息及服务状态 从target_service_run方法拆出来一段 """ # 组装端口 service_port, service_ports = '', [] ports = json.loads(i.get('service_port')) if i.get('service_port') else [] for p in ports: service_ports.append(p.get('default')) if p.get('key') == 'service_port': service_port = p.get('default') # 组装服务状态 serv_status = {0: "正常", 1: "启动中", 2: "停止中", 3: "重启中", 4: "停止"} service_status = serv_status.get(i.get('service_status')) return [service_port, list(set(service_ports)), service_status] def _joint(i, ret, basics, service_port, service_ports, service_status): """ 组件巡检 统一数据组装 "desc: i" server表基础信息 "desc: ret" 各服务基础数据,字典类型 "desc: basic" 各服务拓展数据,列表类型 "desc: service_port" 服务端口 "desc: service_status" 服务状态 "desc: service_ports" 服务全部的端口 """ basic = [ {"name": "port_status", "name_cn": "监听端口", "value": service_ports}, {"name": "IP", "name_cn": "IP地址", "value": i.get('ip')}, ] + basics return { 'id': random.randint(1, 99999999), 'host_ip': i.get('ip'), 'cluster_name': "-", "cpu_usage": ret.get('cpu_usage', '-'), "log_level": ret.get('log_level', '-'), "mem_usage": ret.get('mem_usage', '-'), "run_time": ret.get('run_time', '-'), "service_name": i.get('service_instance_name'), "service_port": service_port, "service_status": service_status, "service_type": "2", "basic": basic } def target_service_thread(env, i): """ 组件数据采集,把顺序执行的代码拷贝出来-多线程执行 :param env: 环境表对象 :param i: 服务表基础数据(端口/服务名/app_name...) """ # 获取每个服务的端口信息及服务状态 _port_status = get_port_and_status(i) if i.get('service__app_monitor', {}).get('type') == 'JavaSpringBoot': _ = ServiceBase(env.name, i.get('ip'), f'{i.get("service__app_name")}Exporter') _.run() tag_total_num = _.metric_num # 总指标数累加 tmp = _joint(i, _.ret, _.basic, *_port_status) # elif i.get('service__app_monitor', {}).get('type') == 'open_source': # TODO else: try: crawl_class = open_source_class_dict.get( i.get("service__app_name")) if not crawl_class: return [0, _joint(i, {}, [], *_port_status)] _ = crawl_class(env=env.name, instance=i.get('ip')) _.run() tag_total_num = _.metric_num # 总指标数累加 tmp = _joint(i, _.ret, _.basic, *_port_status) except Exception as e: logger.error(f'服务巡检失败,详情为{traceback.format_exc(e)}') tag_total_num = 0 # 总指标数 计算 ret, basics = {}, [] tmp = _joint(i, ret, basics, *_port_status) # else: # tag_total_num = 0 # 总指标数 计算 # ret, basics = {}, [] # tmp = _joint(i, ret, basics, *_port_status) return [tag_total_num, tmp] def target_service_run(env, services): """ 组建巡检,多线程执行 :env: 环境 queryset 对象 :services: 服务id 列表 """ tmp_list = list() threads = list() total_no = error_no = 0 # 总指标数、异常指标数 # 查询该环境下服务 services = Service.objects.filter(env=env, id__in=services) services = services.values( 'service_instance_name', 'ip', 'service_port', 'service__app_name', 'service__app_install_args', 'service_status', 'service__app_monitor') for i in services: threads.append(MyThread(func=target_service_thread, args=(env, i))) for t in threads: t.start() for t in threads: t.join() # 用join等待线程执行结束 if not t.res: continue total_no += t.res[0] tmp_list.append(t.res[1]) # 等线程结束,回收返回值 # 扫描统计 scan_info = {"host": 0, "service": len(services), "component": 0} # 分析结果 scan_result = { "all_target_num": total_no, "abnormal_target": error_no, "healthy": "-" } return scan_info, scan_result, tmp_list ================================================ FILE: omp_server/utils/prometheus/target_service_arangodb.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceArangodbCrawl(Prometheus): """ 查询 prometheus arangodb 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 18 self.service_name = "arangodb" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """arangodb 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """arangodb cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """arangodb 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def rocksdb_base_level(self): expr = f"rocksdb_base_level{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_base_level"] = val self.basic.append({ "name": "rocksdb_base_level", "name_cn": "rocksdb基本等级", "value": val }) def client_connections(self): expr = f"arangodb_client_connection_statistics_client_connections{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["client_connections"] = val self.basic.append({ "name": "client_connections", "name_cn": "客户端连接数", "value": val }) def rocksdb_background_errors(self): expr = f"rocksdb_background_errors{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_background_errors"] = val self.basic.append({ "name": "rocksdb_background_errors", "name_cn": "rocksdb_background_errors", "value": val }) def arangodb_transactions_started(self): expr = f"arangodb_transactions_started{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["arangodb_transactions_started"] = val self.basic.append({ "name": "arangodb_transactions_started", "name_cn": "事务开启数", "value": val }) def thread_numbers(self): expr = f"arangodb_process_statistics_number_of_threads{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["thread_numbers"] = val self.basic.append({ "name": "thread_numbers", "name_cn": "线程数", "value": val }) def rocksdb_cache_limit(self): expr = f"rocksdb_cache_limit{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_cache_limit"] = val self.basic.append({ "name": "rocksdb_cache_limit", "name_cn": "缓存限制", "value": val }) def rocksdb_size_all_mem_tables(self): expr = f"rocksdb_size_all_mem_tables{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_size_all_mem_tables"] = val self.basic.append({ "name": "rocksdb_size_all_mem_tables", "name_cn": "表占用内存总字节数", "value": val }) def rocksdb_cache_allocated(self): expr = f"rocksdb_cache_allocated{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_cache_allocated"] = val self.basic.append({ "name": "rocksdb_cache_allocated", "name_cn": "rocksdb_cache_allocated", "value": val }) def rocksdb_num_snapshots(self): expr = f"rocksdb_num_snapshots{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_num_snapshots"] = val self.basic.append({ "name": "rocksdb_num_snapshots", "name_cn": "快照数", "value": val }) def arangodb_transactions_committed(self): expr = f"arangodb_transactions_committed{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["arangodb_transactions_committed"] = val self.basic.append({ "name": "arangodb_transactions_committed", "name_cn": "已提交事务数", "value": val }) def rocksdb_estimate_num_keys(self): expr = f"rocksdb_estimate_num_keys{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_estimate_num_keys"] = val self.basic.append({ "name": "rocksdb_estimate_num_keys", "name_cn": "预测key数", "value": val }) def rocksdb_actual_delayed_write_rate(self): expr = f"rocksdb_actual_delayed_write_rate{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_actual_delayed_write_rate"] = val self.basic.append({ "name": "rocksdb_actual_delayed_write_rate", "name_cn": "延迟写入率", "value": val }) def rocksdb_cache_hit_rate_recent(self): expr = f"rocksdb_cache_hit_rate_recent{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rocksdb_cache_hit_rate_recent"] = val self.basic.append({ "name": "rocksdb_cache_hit_rate_recent", "name_cn": "当前缓存命中率", "value": val }) def arangodb_transactions_aborted(self): expr = f"arangodb_transactions_aborted{{env='{self.env}',instance='{self.instance}',job='arangodbExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["arangodb_transactions_aborted"] = val self.basic.append({ "name": "arangodb_transactions_aborted", "name_cn": "已中断事务数", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'rocksdb_base_level', 'client_connections', 'rocksdb_background_errors', 'arangodb_transactions_started', 'thread_numbers', 'rocksdb_cache_limit', 'rocksdb_size_all_mem_tables', 'rocksdb_cache_allocated', 'rocksdb_num_snapshots', 'arangodb_transactions_committed', 'rocksdb_estimate_num_keys', 'rocksdb_actual_delayed_write_rate', 'rocksdb_cache_hit_rate_recent', 'arangodb_transactions_aborted'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_beanstalk.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceBeanstalkCrawl(Prometheus): """ 查询 prometheus beanstalk 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 18 self.service_name = "beanstalk" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """beanstalk 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """beanstalk cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """beanstalk 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def total_connections(self): expr = f"total_connections{{job='beanstalkExporter',env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["total_connections"] = val self.basic.append({ "name": "total_connections", "name_cn": "总连接数", "value": val }) def total_jobs(self): expr = f"total_jobs{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["total_jobs"] = val self.basic.append({ "name": "total_jobs", "name_cn": "总任务数", "value": val }) def buried_jobs(self): expr = f"current_jobs_buried{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["buried_jobs"] = val self.basic.append({ "name": "buried_jobs", "name_cn": "buried job数", "value": val }) def delayed_jobs(self): expr = f"current_jobs_delayed{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["delayed_jobs"] = val self.basic.append({ "name": "delayed_jobs", "name_cn": "延迟的job数", "value": val }) def timeout_job_num(self): expr = f"job_timeouts{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["timeout_job_num"] = val self.basic.append({ "name": "timeout_job_num", "name_cn": "超时的job数", "value": val }) def stats_cmd_num(self): expr = f"cmd_stats{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["stats_cmd_num"] = val self.basic.append({ "name": "stats_cmd_num", "name_cn": "stats命令数", "value": val }) def reverse_cmd_num(self): expr = f"cmd_reserve{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["reverse_cmd_num"] = val self.basic.append({ "name": "reverse_cmd_num", "name_cn": "reverse命令数", "value": val }) def release_cmd_num(self): expr = f"cmd_release{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["release_cmd_num"] = val self.basic.append({ "name": "release_cmd_num", "name_cn": "release命令数", "value": val }) def put_cmd_num(self): expr = f"cmd_put{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["put_cmd_num"] = val self.basic.append({ "name": "put_cmd_num", "name_cn": "put命令数", "value": val }) def peek_cmd_num(self): expr = f"cmd_peek{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["peek_cmd_num"] = val self.basic.append({ "name": "peek_cmd_num", "name_cn": "peak命令数", "value": val }) def kick_cmd_num(self): expr = f"cmd_kick{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["kick_cmd_num"] = val self.basic.append({ "name": "kick_cmd_num", "name_cn": "kick命令数", "value": val }) def ignore_cmd_num(self): expr = f"cmd_ignore{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ignore_cmd_num"] = val self.basic.append({ "name": "ignore_cmd_num", "name_cn": "ignore命令数", "value": val }) def delete_cmd_num(self): expr = f"cmd_delete{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["delete_cmd_num"] = val self.basic.append({ "name": "delete_cmd_num", "name_cn": "delete命令数", "value": val }) def bury_cmd_num(self): expr = f"cmd_bury{{env='{self.env}',instance='{self.instance}',job='beanstalkExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["bury_cmd_num"] = val self.basic.append({ "name": "bury_cmd_num", "name_cn": "bury命令数", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'total_connections', 'total_jobs', 'buried_jobs', 'delayed_jobs', 'timeout_job_num', 'stats_cmd_num', 'reverse_cmd_num', 'release_cmd_num', 'put_cmd_num', 'peek_cmd_num', 'kick_cmd_num', 'ignore_cmd_num', 'delete_cmd_num', 'bury_cmd_num'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_clickhouse.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceClickhouseCrawl(Prometheus): """ 查询 prometheus clickhouse 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 12 self.service_name = "clickhouse" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """clickhouse 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """clickhouse cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """clickhouse 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def query_nums(self): expr = f"clickhouse_query{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["query"] = val self.basic.append({ "name": "query_nums", "name_cn": "查询总次数", "value": val }) def merge_nums(self): expr = f"clickhouse_merge{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["merge"] = val self.basic.append({ "name": "merge_nums", "name_cn": "merge总次数", "value": val }) def read_only_replica(self): expr = f"sum(clickhouse_readonly_replica{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["read_only_replica"] = val self.basic.append({ "name": "read_only_replica", "name_cn": "仅读副本数", "value": val }) def replication(self): expr = f"clickhouse_replicated_checks{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["replication"] = val self.basic.append({ "name": "replication", "name_cn": "事务数", "value": val }) def clickhouse_read(self): expr = f"clickhouse_read{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["clickhouse_read"] = val self.basic.append({ "name": "clickhouse_read", "name_cn": "已读字节数", "value": val }) def clickhouse_write(self): expr = f"clickhouse_write{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["clickhouse_write"] = val self.basic.append({ "name": "clickhouse_write", "name_cn": "已写字节数", "value": val }) def pool_tasks(self): expr = f"clickhouse_background_pool_task{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["pool_tasks"] = val self.basic.append({ "name": "pool_tasks", "name_cn": "pool task数", "value": val }) def connections(self): expr = f"clickhouse_tcp_connection{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["connections"] = val self.basic.append({ "name": "connections", "name_cn": "连接数", "value": val }) def clickhouse_memory_tracking(self): expr = f"clickhouse_memory_tracking{{env='{self.env}',instance='{self.instance}',job='clickhouseExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["tracking_memory"] = val self.basic.append({ "name": "tracking_memory", "name_cn": "tracking 内存", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'query_nums', 'merge_nums', 'read_only_replica', 'replication', 'clickhouse_read', 'clickhouse_write', 'pool_tasks', 'connections', 'clickhouse_memory_tracking'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_elasticsearch.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceElasticsearchCrawl(Prometheus): """ 查询 prometheus elasticsearch 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 25 self.service_name = "elasticsearch" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """elasticsearch 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """elasticsearch cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """elasticsearch 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def running_nodes(self): expr = f"sum(elasticsearch_cluster_health_number_of_nodes{{env='{self.env}',cluster='cw-es'}})/count(elasticsearch_cluster_health_number_of_nodes{{env='{self.env}',cluster='cw-es'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["running_nodes"] = val self.basic.append({ "name": "running_nodes", "name_cn": "运行节点数", "value": val }) def active_data_nodes(self): expr = f"elasticsearch_cluster_health_number_of_data_nodes{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["active_data_nodes"] = val self.basic.append({ "name": "active_data_nodes", "name_cn": "活跃数据节点数", "value": val }) def pending_tasks(self): expr = f"elasticsearch_cluster_health_number_of_pending_tasks{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["pending_tasks"] = val self.basic.append({ "name": "pending_tasks", "name_cn": "pending任务数", "value": val }) def active_shards(self): expr = f"elasticsearch_cluster_health_active_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["active_shards"] = val self.basic.append({ "name": "active_shards", "name_cn": "活跃shard数", "value": val }) def active_primary_shards(self): expr = f"elasticsearch_cluster_health_active_primary_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["active_primary_shards"] = val self.basic.append({ "name": "active_primary_shards", "name_cn": "活跃 primary shard数", "value": val }) def initializing_shards(self): expr = f"elasticsearch_cluster_health_initializing_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["initializing_shards"] = val self.basic.append({ "name": "initializing_shards", "name_cn": "初始化中的 shard数", "value": val }) def relocating_shards(self): expr = f"elasticsearch_cluster_health_relocating_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["relocating_shards"] = val self.basic.append({ "name": "relocating_shards", "name_cn": "迁移中的shard数", "value": val }) def unassigned_shards(self): expr = f"elasticsearch_cluster_health_unassigned_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["unassigned_shards"] = val self.basic.append({ "name": "unassigned_shards", "name_cn": "未分配shard数", "value": val }) def delayed_unassigned_shards(self): expr = f"elasticsearch_cluster_health_delayed_unassigned_shards{{env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["delayed_unassigned_shards"] = val self.basic.append({ "name": "delayed_unassigned_shards", "name_cn": "延迟shard数", "value": val }) def documents_indexed(self): expr = f"sum(elasticsearch_indices_docs{{env='{self.env}',cluster='cw-es'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["documents_indexed"] = val self.basic.append({ "name": "documents_indexed", "name_cn": "已索引文档数", "value": val }) def index_size(self): expr = f"sum(elasticsearch_indices_store_size_bytes{{env='{self.env}',cluster='cw-es'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["index_size"] = val self.basic.append({ "name": "index_size", "name_cn": "索引大小", "value": val }) def documents_indexed_rate(self): expr = f"rate(elasticsearch_indices_indexing_index_total{{env='{self.env}',cluster='cw-es'}}[1h])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["documents_indexed_rate"] = val self.basic.append({ "name": "documents_indexed_rate", "name_cn": "文档索引率", "value": val }) def query_rate(self): expr = f"rate(elasticsearch_indices_search_fetch_total{{env='{self.env}',cluster='cw-es'}}[1h])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["query_rate"] = val self.basic.append({ "name": "query_rate", "name_cn": "查询率", "value": val }) def queue_count(self): expr = f"sum(elasticsearch_thread_pool_queue_count{{env='{self.env}',cluster='cw-es', type!='management'}}) by (type)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["queue_count"] = val self.basic.append({ "name": "queue_count", "name_cn": "队列数", "value": val }) def gc_seconds(self): expr = f"irate(elasticsearch_jvm_gc_collection_seconds_sum{{env='{self.env}',cluster='cw-es'}}[1m])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["gc_seconds"] = val self.basic.append({ "name": "gc_seconds", "name_cn": "gc总时间", "value": val }) def thread_pool_rejections(self): expr = f"rate(elasticsearch_thread_pool_rejected_count{{env='{self.env}',cluster='cw-es', type!='management'}}[5m])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["thread_pool_rejections"] = val self.basic.append({ "name": "thread_pool_rejections", "name_cn": "线程池拒绝数", "value": val }) def thread_pool_active_count(self): expr = f"sum(elasticsearch_thread_pool_active_count{{env='{self.env}',cluster='cw-es', type!='management'}}) by (type)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["thread_pools"] = val self.basic.append({ "name": "thread_pool_active_count", "name_cn": "线程池活跃数", "value": val }) def avg_heap_in_15min(self): expr = f"avg_over_time(elasticsearch_jvm_memory_used_bytes{{area='heap',env='{self.env}',cluster='cw-es'}}[15m]) / elasticsearch_jvm_memory_max_bytes{{area='heap',env='{self.env}',cluster='cw-es'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["avg_heap_in_15min"] = val self.basic.append({ "name": "avg_heap_in_15min", "name_cn": "15分钟堆内存平均使用大小", "value": val }) def rx_rate_5m(self): expr = f"sum(rate(elasticsearch_transport_rx_packets_total{{env='{self.env}',cluster='cw-es'}}[5m]))" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rx_tx_rate_5m"] = val self.basic.append({ "name": "rx_rate_5m", "name_cn": "rx_rate_5m", "value": val }) def tx_rate_5m(self): expr = f"sum(rate(elasticsearch_transport_tx_packets_total{{env='{self.env}',cluster='cw-es'}}[5m]))" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["rx_tx_rate_5m"] = val self.basic.append({ "name": "tx_rate_5m", "name_cn": "tx_rate_5m", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'running_nodes', 'active_data_nodes', 'pending_tasks', 'active_shards', 'active_primary_shards', 'initializing_shards', 'relocating_shards', 'unassigned_shards', 'delayed_unassigned_shards', 'documents_indexed', 'index_size', 'documents_indexed_rate', 'query_rate', 'queue_count', 'gc_seconds', 'thread_pool_rejections', 'thread_pool_active_count', 'avg_heap_in_15min', 'rx_rate_5m', 'tx_rate_5m'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_flink.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceFlinkCrawl(Prometheus): """ 查询 prometheus flink 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "flink" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """flink 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """flink cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """flink 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "flink_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.ret['service_status'] = ret.get('service_status', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_func.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/31 7:28 下午 # Description: import json from utils.plugin.salt_client import SaltClient def salt_json(instance, func): """ salt执行脚本,获取返回值 """ ret = {} try: _obj = SaltClient() _obj.salt_module_update() ret = _obj.fun(instance, func) if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except: pass return ret if __name__ == '__main__': salt_json('10.0.7.194', 'tomcat_check.main') ================================================ FILE: omp_server/utils/prometheus/target_service_gotty.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceGottyCrawl(Prometheus): """ 查询 prometheus gotty 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "gotty" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """gotty 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """gotty cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """gotty 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "gotty_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.ret['service_status'] = ret.get('service_status', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_grafana.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceGrafanaCrawl(Prometheus): """ 查询 prometheus grafana 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "grafana" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """grafana 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """grafana cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """grafana 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "grafana_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.ret['service_status'] = ret.get('service_status', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_hadoop.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceHadoopCrawl(Prometheus): """ 查询 prometheus hadoop 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "hadoop" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """hadoop 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """hadoop cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """hadoop 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "hadoop_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.ret['service_status'] = ret.get('service_status', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_httpd.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceHttpdCrawl(Prometheus): """ 查询 prometheus httpd 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 14 self.service_name = "httpd" Prometheus.__init__(self) def run_time(self): """httpd 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """httpd cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """httpd 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def process_max_fds(self): expr = f"process_max_fds{{env='{self.env}',instance='$host',job='httpdExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["process_max_fds"] = val self.basic.append({ "name": "process_max_fds", "name_cn": "进程打开文件最大数", "value": val }) def process_cpu_seconds_total(self): expr = f"process_cpu_seconds_total{{env='{self.env}',instance='$host',job='httpdExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["process_cpu_seconds_total"] = val self.basic.append({ "name": "process_cpu_seconds_total", "name_cn": "进程占用cpu总时间", "value": val }) def apache_accesses_total(self): expr = f"apache_accesses_total{{env='{self.env}',instance='$host'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["apache_accesses_total"] = val self.basic.append({ "name": "apache_accesses_total", "name_cn": "access总数", "value": val }) def apache_cpuload(self): expr = f"apache_cpuload{{env='{self.env}',instance='$host'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["apache_cpuload"] = val self.basic.append({ "name": "apache_cpuload", "name_cn": "cpu负载", "value": val }) def apache_workers(self): expr = f"apache_workers{{env='{self.env}',instance='$host'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["apache_workers"] = val self.basic.append({ "name": "apache_workers", "name_cn": "worker数", "value": val }) def http_request_size_bytes(self): expr = f"http_request_size_bytes{{env='{self.env}',instance='$host'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["http_request_size_bytes"] = val self.basic.append({ "name": "http_request_size_bytes", "name_cn": "http请求字节数", "value": val }) def apache_scoreboard(self): expr = f"apache_scoreboard{{env='{self.env}',instance='$host'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["apache_scoreboard"] = val self.basic.append({ "name": "apache_scoreboard", "name_cn": "scoreboard值", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'process_max_fds', 'process_cpu_seconds_total', 'apache_accesses_total', 'apache_cpuload', 'apache_workers', 'http_request_size_bytes', 'http_request_size_bytes', 'apache_scoreboard'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_ignite.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceIgniteCrawl(Prometheus): """ 查询 prometheus ignite 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 19 self.service_name = "ignite" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """ignite 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """ignite cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """ignite 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def ignite_started_thread_count(self): expr = f"ignite_started_thread_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ignite_started_thread_count"] = val self.basic.append({ "name": "ignite_started_thread_count", "name_cn": "开启线程数", "value": val }) def sent_messages_count(self): expr = f"sent_messages_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["sent_messages_count"] = val self.basic.append({ "name": "sent_messages_count", "name_cn": "发送message数", "value": val }) def ignite_received_messages_count(self): expr = f"ignite_received_messages_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ignite_received_messages_count"] = val self.basic.append({ "name": "ignite_received_messages_count", "name_cn": "收到message数", "value": val }) def average_job_wait_time(self): expr = f"average_job_wait_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["average_job_wait_time"] = val self.basic.append({ "name": "average_job_wait_time", "name_cn": "任务平均等待时间", "value": val }) def current_job_wait_time(self): expr = f"current_job_wait_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_job_wait_time"] = val self.basic.append({ "name": "current_job_wait_time", "name_cn": "当前任务等待时间", "value": val }) def maximum_job_wait_time(self): expr = f"maximum_job_wait_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["maximum_job_wait_time"] = val self.basic.append({ "name": "maximum_job_wait_time", "name_cn": "最大任务等待时间", "value": val }) def average_job_execute_time(self): expr = f"average_job_execute_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["average_job_execute_time"] = val self.basic.append({ "name": "average_job_execute_time", "name_cn": "任务平均执行时间", "value": val }) def current_job_execute_time(self): expr = f"current_job_execute_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_job_execute_time"] = val self.basic.append({ "name": "current_job_execute_time", "name_cn": "当前任务执行时间", "value": val }) def maximum_job_execute_time(self): expr = f"maximum_job_execute_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["maximum_job_execute_time"] = val self.basic.append({ "name": "current_job_execute_time", "name_cn": "任务最大执行时间", "value": val }) def busy_time_percentage(self): expr = f"busy_time_percentage{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}*100" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["busy_time_percentage"] = val self.basic.append({ "name": "busy_time_percentage", "name_cn": "忙碌时间占比", "value": val }) def ignite_busy_time_total(self): expr = f"rate(total_busy_time{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}[5m])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ignite_busy_time_total"] = val self.basic.append({ "name": "ignite_busy_time_total", "name_cn": "忙碌态总时间", "value": val }) def ignite_idle_time_total(self): expr = f"rate(ignite_idle_time_total{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}[5m])" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ignite_idle_time_total"] = val self.basic.append({ "name": "ignite_idle_time_total", "name_cn": "空闲态总时间", "value": val }) def current_daemon_thread_count(self): expr = f"current_daemon_thread_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_daemon_thread_count"] = val self.basic.append({ "name": "current_daemon_thread_count", "name_cn": "当前后台线程数", "value": val }) def maximum_thread_count(self): expr = f"maximum_thread_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["maximum_thread_count"] = val self.basic.append({ "name": "maximum_thread_count", "name_cn": "最大线程数", "value": val }) def current_thread_count(self): expr = f"current_thread_count{{env='{self.env}',instance='{self.instance}',job='igniteExporter'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_thread_count"] = val self.basic.append({ "name": "current_thread_count", "name_cn": "当前线程数", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'ignite_started_thread_count', 'sent_messages_count', 'ignite_received_messages_count', 'average_job_wait_time', 'current_job_wait_time', 'maximum_job_wait_time', 'average_job_execute_time', 'current_job_execute_time', 'maximum_job_execute_time', 'busy_time_percentage', 'ignite_busy_time_total', 'ignite_idle_time_total', 'current_daemon_thread_count', 'maximum_thread_count', 'current_thread_count'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_jvm_base.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: from utils.prometheus.prometheus import Prometheus class ServiceBase(Prometheus): """ 查询 prometheus java 指标,基类 """ def __init__(self, env, instance, job): self.ret = {} self.basic = [] self.job = job # Exporter类型 self.env = env # 环境 self.instance = instance # 主机ip self.metric_num = 10 Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'," \ f"app!='node'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """运行时间""" expr = f"process_uptime_seconds{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """cpu使用率""" expr = f"process_cpu_usage{{env=~'{self.env}'," \ f"instance=~'{self.instance}', " \ f"job='{self.job}'}} * 100" val = self.unified_job(*self.query(expr)) val = round(float(val), 2) if val else '0.00' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """内存使用率""" expr = f"sum(jvm_memory_used_bytes{{area='nonheap', env='{self.env}'," \ f"instance=~'{self.instance}'," \ f"job='{self.job}'}}) / " \ f"sum(jvm_memory_max_bytes{{area='nonheap', env=~'{self.env}'," \ f"instance=~'{self.instance}'," \ f"job='{self.job}'}}) * 100" val = self.unified_job(*self.query(expr)) val = round(float(val), 2) if val else '0.00' self.ret['mem_usage'] = f"{val}%" def thread_num(self): """进程数量""" expr = f"jvm_threads_daemon_threads{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "thread_num", "name_cn": "进程数量", "value": self.unified_job(*self.query(expr))} ) def load_average_1m(self): """系统一分钟负载占用情况""" expr = f"system_load_average_1m{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "load_average_1m", "name_cn": "系统一分钟负载占用情况", "value": self.unified_job(*self.query(expr))} ) def tomcat_sessions(self): """Tomcat当前活跃session数量""" expr = f"tomcat_sessions_active_current_sessions{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "tomcat_sessions", "name_cn": "Tomcat当前活跃session数量", "value": self.unified_job(*self.query(expr))} ) def files_max_files(self): """可打开的最大文件描述符数量""" expr = f"process_files_max_files{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "files_max_files", "name_cn": "可打开的最大文件描述符数量", "value": self.unified_job(*self.query(expr))} ) def files_open_files(self): """当前打开的最大文件描述符数量""" expr = f"process_files_open_files{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "files_open_files", "name_cn": "当前打开的最大文件描述符数量", "value": self.unified_job(*self.query(expr))} ) def cpu_count(self): """java虚拟机可用的cpu数量""" expr = f"system_cpu_count{{env=~'{self.env}'," \ f"instance=~'{self.instance}',job=~'{self.job}'}}" self.basic.append({ "name": "cpu_count", "name_cn": "java虚拟机可用的cpu数量", "value": self.unified_job(*self.query(expr))} ) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'thread_num', 'load_average_1m', 'tomcat_sessions', 'files_max_files', 'files_open_files', 'cpu_count'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_kafka.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceKafkaCrawl(Prometheus): """ 查询 prometheus kafka 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "kafka" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """kafka 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """kafka cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """kafka 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def kafka_brokers(self): """kafka brokers""" expr = f"kafka_brokers{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["kafka_brokers"] = val self.basic.append({ "name": "kafka_brokers", "name_cn": "broker数", "value": val }) def process_open_fds(self): """kafka brokers""" expr = f"process_open_fds{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["process_open_fds"] = val self.basic.append({ "name": "process_open_fds", "name_cn": "打开文件描述符数", "value": val }) def process_resident_memory_bytes(self): """kafka brokers""" expr = f"process_resident_memory_bytes{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["process_resident_memory_bytes"] = val self.basic.append({ "name": "process_resident_memory_bytes", "name_cn": "resident memory", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'kafka_brokers', 'process_open_fds', 'process_resident_memory_bytes'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_mysql.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/10/21 5:11 下午 # Description: from utils.prometheus.prometheus import Prometheus from utils.plugin.salt_client import SaltClient class ServiceMysqlCrawl(Prometheus): """ 查询 prometheus mysql 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 10 self.service_name = "mysql" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """mysql 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """mysql cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """mysql 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def slow_query(self): """慢查询""" expr = "rate(mysql_global_status_slow_queries[5m])" self.basic.append({"name": "slow_query", "name_cn": "慢查询", "value": self.unified_job(*self.query(expr))}) def conn_num(self): """当前连接数量""" expr = "rate(mysql_global_status_threads_connected[5m])" self.basic.append({"name": "conn_num", "name_cn": "连接数量", "value": self.unified_job(*self.query(expr))}) def max_connections(self): """最大连接数""" expr = "mysql_global_variables_max_connections" self.basic.append({"name": "max_connections", "name_cn": "最大连接数", "value": self.unified_job(*self.query(expr))}) def threads_running(self): """活跃连接数量""" expr = "mysql_global_status_threads_running" self.basic.append({"name": "threads_running", "name_cn": "活跃连接数", "value": self.unified_job(*self.query(expr))}) def qps(self): """qps""" expr = "rate(mysql_global_status_questions[5m])" _ = self.unified_job(*self.query(expr)) _ = round(float(_), 2) if _ else 0 self.basic.append({"name": "qps", "name_cn": "qps", "value": _}) def backup_status(self): """备份状态""" expr = "mysql_global_status_slave_open_temp_tables" self.basic.append({"name": "backup_status", "name_cn": "数据同步状态", "value": self.unified_job(*self.query(expr))}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'slow_query', 'conn_num', 'max_connections', 'threads_running', 'qps', 'backup_status'] for t in target: if getattr(self, t): getattr(self, t)() if __name__ == '__main__': h = ServiceMysqlCrawl(env='demo', instance='10.0.9.60') h.run() print(h.ret) ================================================ FILE: omp_server/utils/prometheus/target_service_nacos.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: from utils.prometheus.prometheus import Prometheus class ServiceNacosCrawl(Prometheus): """ 查询 prometheus Nacos 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self.metric_num = 17 self.service_name = "nacos" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"count(nacos_monitor{{name='configCount',env=~'{self.env}'," \ f"instance=~'{self.instance}'}})" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """nacos 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """nacos cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """nacos 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def service_count(self): expr = f"max(nacos_monitor{{name='serviceCount',env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["service_count"] = val self.basic.append({ "name": "service_count", "name_cn": "注册服务数", "value": val }) def ip_count(self): expr = f"max(nacos_monitor{{name='ipCount',env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["ip_count"] = val self.basic.append({ "name": "ip_count", "name_cn": "注册ip数", "value": val }) def config_count(self): expr = f"max(nacos_monitor{{name='configCount',env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["config_count"] = val self.basic.append({ "name": "config_count", "name_cn": "注册config数", "value": val }) def config_push_total(self): expr = f"sum(nacos_monitor{{name='getConfig',env='{self.env}',instance='{self.instance}'}}) by (name)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["config_push_total"] = val self.basic.append({ "name": "config_push_total", "name_cn": "获取config数", "value": val }) def threads(self): expr = f"max(jvm_threads_daemon_threads{{env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["threads"] = val self.basic.append({ "name": "threads", "name_cn": "后台线程数", "value": val }) def notify_rt(self): expr = f"sum(rate(nacos_timer_seconds_sum{{env='{self.env}',instance='{self.instance}'}}[1m]))/sum(rate(nacos_timer_seconds_count{{env='{self.env}',instance='{self.instance}'}}[1m])) * 1000" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["notify_rt"] = val self.basic.append({ "name": "notify_rt", "name_cn": "notify_rt", "value": val }) def long_polling(self): expr = f"sum(nacos_monitor{{name='longPolling', env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["long_polling"] = val self.basic.append({ "name": "long_polling", "name_cn": "long_polling", "value": val }) def qps(self): expr = f"sum(rate(http_server_requests_seconds_count{{uri='/v1/cs/configs|/nacos/v1/ns/instance|/nacos/v1/ns/health', env='{self.env}',instance='{self.instance}'}}[1m])) by (method,uri)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["qps"] = val self.basic.append({ "name": "qps", "name_cn": "qps", "value": val }) def leader_status(self): expr = f"sum(nacos_monitor{{name='leaderStatus', env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["leader_status"] = val self.basic.append({ "name": "leader_status", "name_cn": "leader状态", "value": val }) def avg_push_cost(self): expr = f"sum(nacos_monitor{{name='avgPushCost', env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["avg_push_cost"] = val self.basic.append({ "name": "avg_push_cost", "name_cn": "avg_push_cost", "value": val }) def max_push_cost(self): expr = f"max(nacos_monitor{{name='maxPushCost', env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["max_push_cost"] = val self.basic.append({ "name": "max_push_cost", "name_cn": "max_push_cost", "value": val }) def config_statistics(self): expr = f"sum(nacos_monitor{{name='publish', env='{self.env}',instance='{self.instance}'}}) by (name)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["config_statistics"] = val self.basic.append({ "name": "config_statistics", "name_cn": "config_statistics", "value": val }) def health_check(self): expr = f"sum(rate(nacos_monitor{{name='.*HealthCheck', env='{self.env}',instance='{self.instance}'}}[1m])) by (name) * 60" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["health_check"] = val self.basic.append({ "name": "health_check", "name_cn": "health_check", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'service_count', 'ip_count', 'config_count', 'config_push_total', 'threads', 'notify_rt', 'long_polling', 'qps', 'leader_status', 'avg_push_cost', 'max_push_cost', 'config_statistics', 'health_check'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_ntpd.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceNtpdCrawl(Prometheus): """ 查询 prometheus ntpd 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "ntpd" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """ntpd 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """ntpd cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """ntpd 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "ntpd_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.ret['service_status'] = ret.get('service_status', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_postgresql.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServicePostgresqlCrawl(Prometheus): """ 查询 prometheus postgresql 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 16 self.service_name = "postgresql" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """postgresql 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """postgresql cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """postgresql 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def current_fetch_data(self): expr = f"SUM(pg_stat_database_tup_fetched{{env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_fetch_data"] = val self.basic.append({ "name": "current_fetch_data", "name_cn": "当前fetch数据", "value": val }) def current_insert_data(self): expr = f"SUM(pg_stat_database_tup_inserted{{release='$release', env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_insert_data"] = val self.basic.append({ "name": "current_insert_data", "name_cn": "当前insert数据", "value": val }) def current_update_data(self): expr = f"SUM(pg_stat_database_tup_updated{{env='{self.env}',instance='{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["current_update_data"] = val self.basic.append({ "name": "current_update_data", "name_cn": "当前update数据", "value": val }) def max_connections(self): expr = f"pg_settings_max_connections{{release='$release', env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["max_connections"] = val self.basic.append({ "name": "max_connections", "name_cn": "最大连接数", "value": val }) def open_file_descriptors(self): expr = f"process_open_fds{{release='$release', env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["open_file_descriptors"] = val self.basic.append({ "name": "open_file_descriptors", "name_cn": "打开文件描述符数", "value": val }) def shared_buffers(self): expr = f"pg_settings_shared_buffers_bytes{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["shared_buffers"] = val self.basic.append({ "name": "shared_buffers", "name_cn": "shared_buffers", "value": val }) def effective_cache(self): expr = f"pg_settings_effective_cache_size_bytes{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["effective_cache"] = val self.basic.append({ "name": "effective_cache", "name_cn": "有效缓存", "value": val }) def max_wal_size(self): expr = f"pg_settings_max_wal_size_bytes{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["max_wal_size"] = val self.basic.append({ "name": "max_wal_size", "name_cn": "max_wal_size", "value": val }) def random_page_cost(self): expr = f"pg_settings_random_page_cost{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["random_page_cost"] = val self.basic.append({ "name": "random_page_cost", "name_cn": "random_page_cost", "value": val }) def seq_page_cost(self): expr = f"pg_settings_seq_page_cost{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["seq_page_cost"] = val self.basic.append({ "name": "seq_page_cost", "name_cn": "seq_page_cost", "value": val }) def max_worker_processes(self): expr = f"pg_settings_max_worker_processes{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["max_worker_processes"] = val self.basic.append({ "name": "max_worker_processes", "name_cn": "最大进程worker数", "value": val }) def max_parallel_workers(self): expr = f"pg_settings_max_parallel_workers{{env='{self.env}',instance='{self.instance}'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["max_parallel_workers"] = val self.basic.append({ "name": "max_parallel_workers", "name_cn": "max_parallel_workers", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'current_fetch_data', 'current_insert_data', 'current_update_data', 'max_connections', 'open_file_descriptors', 'shared_buffers', 'effective_cache', 'max_wal_size', 'random_page_cost', 'seq_page_cost', 'max_worker_processes', 'max_parallel_workers'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_prometheus.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/15 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServicePrometheusCrawl(Prometheus): """ 查询 prometheus prometheus 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "prometheus" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """prometheus 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """prometheus cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """prometheus 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'test_test'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_redis.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: from utils.prometheus.prometheus import Prometheus class ServiceRedisCrawl(Prometheus): """ 查询 prometheus redis 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self.metric_num = 8 self.service_name = "redis" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """redis 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """redis cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """redis 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def conn_num(self): """连接数量""" expr = f"redis_connected_clients{{env='{self.env}'," \ f"instance=~'{self.instance}'}}" self.basic.append({ "name": "conn_num", "name_cn": "连接数量", "value": self.unified_job(*self.query(expr))} ) def hit_rate(self): """命中率""" expr = f"(redis_keyspace_hits_total{{env='{self.env}', " \ f"instance=~'{self.instance}', job='redisExporter'}} / " \ f"(redis_keyspace_hits_total{{env='{self.env}', " \ f"instance=~'{self.instance}', job='redisExporter'}} + " \ f"redis_keyspace_misses_total{{env='{self.env}', " \ f"instance=~'{self.instance}', job='redisExporter'}})) * 100" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else 0 self.basic.append({ "name": "hit_rate", "name_cn": "缓存命中率", "value": f"{val}%"} ) def max_memory(self): """最大内存""" expr = f"redis_memory_max_bytes{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}})" val = self.unified_job(*self.query(expr)) val = round(int(val) / 1048576, 2) if val else '-' self.basic.append({ "name": "max_memory", "name_cn": "最大内存", "value": f"{val}m"} ) def network_io(self): """网络io""" expr = f"redis_net_input_bytes_total{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}} / 1000000" val_in = self.unified_job(*self.query(expr)) val_in = round(float(val_in), 2) if val_in else 0 expr = f"redis_net_output_bytes_total{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}} / 1000000" val_out = self.unified_job(*self.query(expr)) val_out = round(float(val_out), 2) if val_out else 0 self.basic.append({ "name": "network_io", "name_cn": "网络io", "value": f"{val_in}B/{val_out}B"} ) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'conn_num', 'hit_rate', 'max_memory', 'network_io'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_rocketmq.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: import json from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceRocketmqCrawl(Prometheus): """ 查询 prometheus rocketmq 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 4 self.service_name = "rocketmq" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """rocketmq 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """rocketmq cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """rocketmq 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def broker_tps(self): """生产消息数量/s""" expr = f"rocketmq_broker_tps{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}}" self.basic.append({ "name": "broker_tps", "name_cn": "生产消息数量/s", "value": self.unified_job(*self.query(expr))} ) def broker_qps(self): """消费消息数量/s""" expr = f"rocketmq_broker_qps{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}}" self.basic.append({ "name": "broker_qps", "name_cn": "消费消息数量/s", "value": self.unified_job(*self.query(expr))} ) def message_accumulation(self): """消息堆积量""" expr = f"rocketmq_message_accumulation{{env=~'{self.env}'," \ f"instance=~'{self.instance}'}}" self.basic.append({ "name": "message_accumulation", "name_cn": "消息堆积量", "value": self.unified_job(*self.query(expr))} ) def salt_json(self): try: self._obj.salt_module_update() ret = self._obj.fun(self.instance, "rocketmq_check.main") if ret and ret[0]: ret = json.loads(ret[1]) else: ret = {} except Exception: ret = {} self.ret['cpu_usage'] = ret.get('cpu_usage', '-') self.ret['mem_usage'] = ret.get('mem_usage', '-') self.ret['run_time'] = ret.get('run_time', '-') self.ret['log_level'] = ret.get('log_level', '-') self.basic.append({"name": "max_memory", "name_cn": "最大内存", "value": ret.get('max_memory', '-')}) def run(self): """统一执行实例方法""" # target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', # 'broker_tps', 'broker_qps', 'message_accumulation', # 'salt_json'] target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_tengine.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: lingyang guo # CreateDate: 2021/12/8 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceTengineCrawl(Prometheus): """ 查询 prometheus tengine 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 11 self.service_name = "tengine" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """tengine 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """tengine cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """tengine 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def server_connections(self): expr = f"nginx_server_connections{{env='{self.env}',instance='{self.instance}',status='active|writing|reading|waiting'}}" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["server_connections"] = val self.basic.append({ "name": "server_connections", "name_cn": "连接数", "value": val }) def server_cache(self): expr = f"sum(irate(nginx_server_cache{{env='{self.env}',instance='{self.instance}'}}[5m])) by (status)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["server_cache"] = val self.basic.append({ "name": "server_cache", "name_cn": "缓存", "value": val }) def server_requests(self): expr = f"sum(irate(nginx_server_requests{{env='{self.env}',instance='{self.instance}', code!='total'}}[5m])) by (code)" val = self.unified_job(*self.query(expr)) val = val if val else 0 self.ret["server_requests"] = val self.basic.append({ "name": "server_requests", "name_cn": "request数", "value": val }) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'server_connections', 'server_cache', 'server_requests'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/target_service_zookeeper.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/8 8:00 下午 # Description: from utils.plugin.salt_client import SaltClient from utils.prometheus.prometheus import Prometheus class ServiceZookeeperCrawl(Prometheus): """ 查询 prometheus zookeeper 指标 """ def __init__(self, env, instance): self.ret = {} self.basic = [] self.env = env # 环境 self.instance = instance # 主机ip self._obj = SaltClient() self.metric_num = 10 self.service_name = "zookeeper" Prometheus.__init__(self) def service_status(self): """运行状态""" expr = f"probe_success{{env='{self.env}', instance='{self.instance}', " \ f"app='{self.service_name}'}}" self.ret['service_status'] = self.unified_job(*self.query(expr)) def run_time(self): """zookeeper 运行时间""" expr = f"process_uptime_seconds{{env='{self.env}', instance='{self.instance}', app='{self.service_name}'}}" _ = self.unified_job(*self.query(expr)) _ = float(_) if _ else 0 minutes, seconds = divmod(_, 60) hours, minutes = divmod(minutes, 60) days, hours = divmod(hours, 24) if int(days) > 0: self.ret['run_time'] = \ f"{int(days)}天{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" elif int(hours) > 0: self.ret['run_time'] = \ f"{int(hours)}小时{int(minutes)}分钟{int(seconds)}秒" else: self.ret['run_time'] = f"{int(minutes)}分钟{int(seconds)}秒" def cpu_usage(self): """zookeeper cpu使用率""" expr = f"service_process_cpu_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['cpu_usage'] = f"{val}%" def mem_usage(self): """zookeeper 内存使用率""" expr = f"service_process_memory_percent{{instance='{self.instance}',app='{self.service_name}'}}" val = self.unified_job(*self.query(expr)) val = round(float(val), 4) if val else '-' self.ret['mem_usage'] = f"{val}%" def packets_received(self): """收包数""" expr = f"zk_packets_received{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "packets_received", "name_cn": "收包数", "value": self.unified_job(*self.query(expr))} ) def packets_sent(self): """发包数""" expr = f"zk_packets_sent{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "packets_sent", "name_cn": "发包数", "value": self.unified_job(*self.query(expr))} ) def num_alive_connections(self): """活跃连接数""" expr = f"zk_num_alive_connections{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "num_alive_connections", "name_cn": "活跃连接数", "value": self.unified_job(*self.query(expr))} ) def outstanding_requests(self): """堆积请求数""" expr = f"zk_outstanding_requests{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "outstanding_requests", "name_cn": "堆积请求数", "value": self.unified_job(*self.query(expr))} ) def znode_count(self): """znode数""" expr = f"zk_znode_count{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "znode_count", "name_cn": "节点数", "value": self.unified_job(*self.query(expr))} ) def watch_count(self): """watch数""" expr = f"zk_watch_count{{env='{self.env}', " \ f"instance=~'{self.instance}', job='zookeeperExporter'}}" self.basic.append({ "name": "watch_count", "name_cn": "监测点数", "value": self.unified_job(*self.query(expr))} ) def run(self): """统一执行实例方法""" target = ['service_status', 'run_time', 'cpu_usage', 'mem_usage', 'packets_received', 'packets_sent', 'num_alive_connections', 'outstanding_requests', 'znode_count', 'watch_count'] for t in target: if getattr(self, t): getattr(self, t)() ================================================ FILE: omp_server/utils/prometheus/thread.py ================================================ # !/usr/bin/python3 # -*-coding:utf-8-*- # Author: len chen # CreateDate: 2021/11/10 6:11 下午 # Description: import threading class MyThread(threading.Thread): """ 封装下多线程 重写下run 拿到每个线程的返回值 """ def __init__(self, func, args): threading.Thread.__init__(self) self.func = func self.args = args self.res = None def run(self): self.res = self.func(*self.args) def result(self): return self.res ================================================ FILE: omp_server/utils/prometheus/update_threshold.py ================================================ # -*- coding: utf-8 -*- import argparse import os import requests import yaml import logging from omp_server.settings import PROJECT_DIR logger = logging.getLogger("server") description = { "cpu_used": "主机 {{ $labels.instance }} CPU 使用率为 {{ $value | humanize }}%, 大于阈值 $condition_value$%", "memory_used": "主机 {{ $labels.instance }} 内存使用率为 {{ $value | humanize }}%,大于阈值 $condition_value$%", "disk_root_used": "主机 {{ $labels.instance }} 根分区使用率为 {{ $value | humanize }}%, 大于 阈值 $condition_value$%", "disk_data_used": "主机 {{ $labels.instance }} 数据分区使用率为 {{ $value | humanize }}%, 大于 阈值 $condition_value$%", "kafka_consumergroup_lag": "Kafka 消费组{{ $labels.consumergroup }}消息堆积数过多 {{ humanize $value }}" } expr = { "cpu_used": "(100 - sum(avg without (cpu)(irate(node_cpu_seconds_total{mode='idle', " "env=\"$env_name$\"}[2m]))) by (instance) * 100) $condition$ $condition_value$", "memory_used": "(1 - (node_memory_MemAvailable_bytes{env=\"$env_name$\"} / (node_" "memory_MemTotal_bytes{env=\"$env_name$\"}))) * 100 $condition$ $condition_value$", "disk_root_used": "max((node_filesystem_size_bytes{env=\"$env_name$\"," "mountpoint=\"/\"}-node_filesystem_free_bytes" "{env=\"$env_name$\",mountpoint=\"/\"}) *100/(node_filesystem_avail_" "bytes{env=\"$env_name$\",mountpoint=\"/\"}+(node_filesystem_size_bytes{" "env=\"$env_name$\",mountpoint=\"/\"}-node" "_filesystem_free_bytes{env=\"$env_name$\",mountpoint=\"/\"})))by(instance)" " $condition$ $condition_value$", "disk_data_used": "max((node_filesystem_size_bytes{env=\"$env_name$\"," "mountpoint=\"$disk_data_path$\"}-node_filesystem_free_bytes" "{env=\"$env_name$\",mountpoint=\"$disk_data_path$\"}) *100/(node_filesystem_avail_" "bytes{env=\"$env_name$\",mountpoint=\"$disk_data_path$\"}+(node_filesystem_size_bytes{" "env=\"$env_name$\",mountpoint=\"$disk_data_path$\"}-node" "_filesystem_free_bytes{env=\"$env_name$\",mountpoint=\"$disk_data_path$\"})))by(instance)" " $condition$ $condition_value$", "kafka_consumergroup_lag": "sum(kafka_consumergroup_lag{env=\"$env_name$\"}) by (consumergroup,instance,job,env) $condition$ $condition_value$" } def gen_summary(index_type): return replace_value("$index_type$ (instance {{ $labels.instance }})", index_type=index_type) def replace_value(line, env_name=None, condition=None, condition_value=None, alert_level=None, index_type=None, disk_data_path=None): if env_name: line = line.replace("$env_name$", str(env_name)) if condition: line = line.replace("$condition$", str(condition)) if condition_value: line = line.replace("$condition_value$", str(condition_value)) if alert_level: line = line.replace("$alert_level$", str(alert_level)) if index_type: line = line.replace("$index_type$", str(index_type)) if disk_data_path: line = line.replace("$disk_data_path$", str(disk_data_path)) return line def update_node_rule_yaml(quotes_info): """ 更新主机指标文件 """ metric_en_cn_dict = { "cpu_used": "CPU", "memory_used": "内存", "disk_root_used": "根分区磁盘", "disk_data_used": "数据分区磁盘" } env_name = quotes_info.get("env_name") node_rule_yml_path = os.path.join(PROJECT_DIR, 'component', 'prometheus/conf/rules', '{}_node_rule.yml'.format(env_name)) instance_alert = { "alert": "实例宕机", "annotations": { "consignee": "{}".format(""), # TODO "description": "实例 {{ $labels.instance }} monitor_agent进程丢失或主机发生宕机已超过1分钟", "summary": "实例宕机({{ $labels.instance }})" }, "expr": "sum(up{job=\"nodeExporter\", env=\'%s\'}) by (instance) < 1" % env_name, "for": "1m", "labels": { "job": "nodeExporter", "severity": "critical" } } node_rules = {"name": "node alert", "rules": [instance_alert]} dict_total_rules = {"groups": [node_rules]} node_quotes = quotes_info.get("hosts") env_name = quotes_info.get("env_name") disk_data_path = quotes_info.get("disk_data_path", None) try: for host_quote_info in node_quotes: index_type = host_quote_info.get("index_type") if index_type == "disk_data_used": if not disk_data_path: continue alert_level = host_quote_info.get("alert_level") condition_value = host_quote_info.get("condition_value") condition = host_quote_info.get("condition") quote_info = { "alert": "主机 {} 使用率过高".format(metric_en_cn_dict.get(index_type)), "annotations": { "consignee": "{}".format(""), # TODO "description": replace_value(description.get(index_type), condition_value=condition_value), "summary": gen_summary(index_type=index_type), }, "expr": replace_value(expr.get(index_type), env_name=env_name, condition=condition, condition_value=condition_value, disk_data_path=disk_data_path), "for": "1m", "labels": { "job": "nodeExporter", "severity": alert_level, } } logger.info(quote_info) node_rules["rules"].append(quote_info) logger.info('开始更新告警规则文件{}'.format(node_rule_yml_path)) with open(node_rule_yml_path, "w") as fw: yaml.dump(dict_total_rules, fw, allow_unicode=True) logger.info("更新主机告警规则文件成功") return True except Exception as e: logger.error(e) return False def update_service_rule_yaml(quotes_info): """ 更新服务指标文件 """ env_name = quotes_info.get("env_name") service_rule_yml_path = os.path.join(PROJECT_DIR, 'component', 'prometheus/conf/rules', '{}_service_status_rule.yml'.format(env_name)) instance_alert = { "alert": "app state", "annotations": { "consignee": "{}".format(""), # TODO "description": "主机 {{ $labels.instance }} 中的 服务 {{ $labels.app }} 已经down掉超过一分钟.", "summary": "app state(instance {{ $labels.instance }})" }, "expr": "probe_success{env=\'%s\'} == 0 " % env_name, "for": "1m", "labels": { "severity": "critical" } } service_rules = {"name": "App state", "rules": [instance_alert]} dict_total_rules = {"groups": [service_rules]} service_quotes = quotes_info.get("services") try: for service_name, service_quote_info in service_quotes.items(): for service_quote in service_quote_info: index_type = service_quote.get("index_type") alert_level = service_quote.get("alert_level") condition_value = service_quote.get("condition_value") condition = service_quote.get("condition") quote_info = { "alert": "{} {} alert".format(service_name, index_type), "annotations": { "consignee": "{}".format(""), # TODO "description": replace_value(description.get(index_type), condition_value=condition_value), "summary": gen_summary(index_type=index_type), }, "expr": replace_value(expr.get(index_type), env_name=env_name, condition=condition, condition_value=condition_value), "for": "1m", "labels": { "severity": alert_level, } } logger.info(quote_info) service_rules["rules"].append(quote_info) logger.info('开始更新服务告警规则文件{}'.format(service_rule_yml_path)) with open(service_rule_yml_path, "w") as fw: yaml.dump(dict_total_rules, fw, allow_unicode=True) logger.info("更新服务告警规则文件成功") return True except Exception as e: logger.error(e) return False def config_update(quotes_info): """ :param quotes_info: 相关指标阈值信息 :return: {"env_name": "env_name", "hosts": [ {"index_type": "cpu_used","condition": ">=","condition_value": "80","alert_level": "warning"}, {"index_type": "cpu_used","condition": ">=","condition_value": "90","alert_level": "critical"}, {"index_type": "memory_used","condition": ">=","condition_value": "80","alert_level": "warning"}, {"index_type": "memory_used","condition": ">=","condition_value": "90","alert_level": "critical"}, {"index_type": "disk_root_used","condition": ">=","condition_value": "80","alert_level": "warning"}, {"index_type": "disk_root_used","condition": ">=","condition_value": "90","alert_level": "critical"}, {"index_type": "disk_data_used","condition": ">","condition_value": "80","alert_level": "warning"}, {"index_type": "disk_data_used","condition": ">","condition_value": "90","alert_level": "critical"} ], "services": { "kafka": [ {"index_type": "kafka_consumergroup_lag","condition": ">","condition_value": 400,"alert_level": "warning"}, {"index_type": "kafka_consumergroup_lag","condition": ">","condition_value": 600,"alert_level": "critical"} ] } } """ logger.info('收到阈值更新json:{}'.format(quotes_info)) # quotes_info = json.loads(quotes_info) # 更新主机规则文件 update_node_mark = update_node_rule_yaml(quotes_info) if not update_node_mark: logger.error("更新主机告警规则失败") # 更新服务规则文件 update_service_mark = update_service_rule_yaml(quotes_info) if not update_service_mark: logger.error("更新服务告警规则失败") try: from promemonitor.prometheus import Prometheus url = "http://{}/-/reload".format(Prometheus.get_prometheus_config()) # NOQA response = requests.request("POST", url) if response.status_code == 200: logger.info('重载prometheus配置成功!') else: logger.error('重载prometheus配置失败!') except ConnectionRefusedError: logger.error('重载prometheus配置失败!') return True, None if __name__ == '__main__': parser = argparse.ArgumentParser( usage="it's usage tip.", description="help info.") parser.add_argument("--threshold-json", dest="threshold_json", help="the json of threshold") args = parser.parse_args() threshold_json = args.threshold_json config_update(threshold_json) ================================================ FILE: omp_server/utils/prometheus/utils.py ================================================ # -*- coding: utf-8 -*- # Project: utils # Author: jon.liu@yunzhihui.com # Create time: 2021-10-31 14:07 # IDE: PyCharm # Version: 1.0 # Introduction: """ 公共数据问题 """ from db_models.models import Host def get_host_data_folder(instance): """ 解析主机数据,获取主机磁盘分区数据 :param instance: :return: """ item = Host.objects.filter(ip=instance).last() if not item: return "" _data_folder = item.data_folder # {"/": 90, "/data": 100} _disk_info = item.disk if not _disk_info: _disk_info = Host.objects.get(id=item.id).disk data_path = "" if _disk_info and isinstance(_disk_info, dict): for key, _ in _disk_info.items(): if key == "/": continue _check_data_folder = _data_folder.rstrip("/") + "/" _check_key = key.rstrip("/") + "/" if _check_data_folder.startswith(_check_key): data_path = key break return data_path ================================================ FILE: omp_server/utils/response_handler.py ================================================ # -*- coding: utf-8 -*- # Project: response_handler # Author: jon.liu@yunzhihui.com # Create time: 2021-09-10 21:36 # IDE: PyCharm # Version: 1.0 # Introduction: """ 重新封装的响应数据类 """ from rest_framework.renderers import JSONRenderer class APIRenderer(JSONRenderer): """自定义响应数据类""" def render(self, data, accepted_media_type=None, renderer_context=None): """ 自定义render返回数据 :param data: 返回数据 :param accepted_media_type: :param renderer_context: :return: """ dic = {"code": 0, "message": "success", "data": None} if isinstance(data, dict): if data.get("code") == 1: dic = {"code": 1, "message": data.get("message"), "data": None} elif "non_field_errors" in data: if isinstance(data.get("non_field_errors"), list): _message = "" for item in data.get("non_field_errors"): _message += f"{item} " dic = {"code": 1, "message": _message, "data": None} else: dic = {"code": 0, "message": "success", "data": data} else: dic = {"code": 0, "message": "success", "data": data} return super().render( data=dic, accepted_media_type=accepted_media_type, renderer_context=renderer_context) ================================================ FILE: omp_web/README.md ================================================ # Getting Started with Create React App This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). ## Available Scripts In the project directory, you can run: ### `yarn start` Runs the app in the development mode.\ Open [http://localhost:3000](http://localhost:3000) to view it in the browser. The page will reload if you make edits.\ You will also see any lint errors in the console. ### `yarn test` Launches the test runner in the interactive watch mode.\ See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. ### `yarn build` Builds the app for production to the `build` folder.\ It correctly bundles React in production mode and optimizes the build for the best performance. The build is minified and the filenames include the hashes.\ Your app is ready to be deployed! See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. ### `yarn eject` **Note: this is a one-way operation. Once you `eject`, you can’t go back!** If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. ## Learn More You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). To learn React, check out the [React documentation](https://reactjs.org/). ### Code Splitting This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) ### Analyzing the Bundle Size This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) ### Making a Progressive Web App This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) ### Advanced Configuration This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) ### Deployment This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) ### `yarn build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) ================================================ FILE: omp_web/config-overrides.js ================================================ const { addWebpackAlias, override, overrideDevServer, addLessLoader, addPostcssPlugins, fixBabelImports, addWebpackPlugin, } = require("customize-cra"); const ProgressBarPlugin = require("progress-bar-webpack-plugin"); const UglifyJsPlugin = require("uglifyjs-webpack-plugin"); const path = require("path"); // 跨域配置 const devServerConfig = () => (config) => { return { ...config, proxy: { "/api": { target: "http://10.0.14.200:19001/", changeOrigin: true, }, }, }; }; module.exports = { webpack: override( fixBabelImports("import", { libraryName: "antd", libraryDirectory: "es", style: true, //自动打包相关的样式 默认为 style:'css' }), // 使用less-loader对源码重的less的变量进行重新制定,设置antd自定义主题 addLessLoader({ javascriptEnabled: true, modifyVars: { "@primary-color": "#4986f7", "@text-color": "rgba(0,0,0,0.65)", // "@border-radius-base": "4px" }, }), // addPostcssPlugins([require("postcss-px2rem-exclude")({ // remUnit: 16, // propList: ['*'], // exclude: '' // })]), addWebpackPlugin(new ProgressBarPlugin()), process.env.NODE_ENV === "production" && addWebpackPlugin( new UglifyJsPlugin({ // 开启打包缓存 cache: true, // 开启多线程打包 parallel: true, uglifyOptions: { // 删除警告 warnings: false, // 压缩 compress: { // 移除console drop_console: true, // 移除debugger drop_debugger: true, }, }, }) ), addWebpackAlias({ "@": path.resolve(__dirname, "./src"), assets: path.resolve(__dirname, "./src/assets"), components: path.resolve(__dirname, "./src/components"), pages: path.resolve(__dirname, "./src/pages"), common: path.resolve(__dirname, "./src/common"), }), (config) => { if (process.env.NODE_ENV === "production") config.devtool = false; if (process.env.NODE_ENV === "production") { const paths = require("react-scripts/config/paths"); paths.appBuild = path.join(path.dirname(paths.appBuild), "dist"); config.output.path = path.join( path.dirname(config.output.path), "dist" ); } return config; } ), devServer: overrideDevServer(devServerConfig()), }; ================================================ FILE: omp_web/package.json ================================================ { "name": "omp-fontend", "version": "0.1.0", "private": true, "dependencies": { "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "antd": "4.17.1-alpha.1", "axios": "^0.21.1", "babel-plugin-import": "^1.13.3", "browser-md5-file": "^1.1.1", "customize-cra": "^1.0.0", "echarts": "^5.1.2", "echarts-for-react": "^3.0.1", "highlight.js": "^11.4.0", "http-proxy-middleware": "2.0", "jsencrypt": "3.0.0-rc.1", "less-loader": "5.0.0", "markdown-it": "^12.3.2", "markdown-it-colors": "^2.0.4", "moment": "2.29.1", "postcss-px2rem-exclude": "^0.0.6", "postcss-pxtorem": "^6.0.0", "ramda": "0.27.1", "react": "^17.0.2", "react-app-rewired": "^2.1.8", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "redux": "^4.1.0", "typescript": "^4.3.4", "uglifyjs-webpack-plugin": "^2.2.0", "web-vitals": "^1.0.1", "xlsx": "0.16.0" }, "scripts": { "start": "react-app-rewired start", "build": "CI=false && react-app-rewired build", "test": "react-app-rewired test", "eject": "react-app-rewired eject", "publish": "rsync -avzP --delete -e 'ssh -p36000' ./dist/* root@10.0.9.67:/root/domh/ompServer/frontend/" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ], "rules": { "no-undef": "off", "no-restricted-globals": "off", "no-unused-vars": "off" } }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "compression-webpack-plugin": "^8.0.0", "less": "^4.1.1", "progress-bar-webpack-plugin": "^2.1.0", "react-app-rewire-less": "^2.1.3", "react-app-rewire-less-modules": "^1.3.0" } } ================================================ FILE: omp_web/public/index.html ================================================ 云智慧
================================================ FILE: omp_web/public/pubKey.json ================================================ {"publicKey":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwv4dqlvcYtrPJsCL/VuX\n0u4FZm2E0du1m01gUnp3afSkx+u2GTXptpS7dNfTLguu1HjJUzkEIGaJGG/x/PR2\nZs6I/UmIFWj6tdmfBBlrVRETnm8tCAdO9/1zjzz4wB2yuHduBK6TYwhXfZOCg3LO\nj+QVpUYqyq3lqjPN+C6QbFzgk8FwMHr+R3OzZe9nsaNZOHRbSmu6NU5zkIdnScQw\nIiWIe9nZMpoTUe45FtYPj7SHiCItDOtbXGbyNOP5k5RhIQtJiEJgVOzGeRSaQAj6\nUfLClsa43ZXfXIZ+BfOO8GZBXmHeRHRs/Prw3Io4n0gXKpgrd6MxwfpoxmXEyoRU\nGwIDAQAB"} ================================================ FILE: omp_web/src/App.js ================================================ import Router from "./router.js"; import { Provider } from "react-redux"; import store from "@/store_redux/reduxStore"; // 国际化 import zhCN from 'antd/es/locale/zh_CN'; import { ConfigProvider } from "antd"; import 'moment/locale/zh-cn' const App = () => { return ( ); }; export default App; ================================================ FILE: omp_web/src/components/CustomBreadcrumb/index.js ================================================ import { Breadcrumb, message } from "antd"; import React, { useState, useEffect, useContext, useLayoutEffect } from "react"; import { withRouter } from "react-router-dom"; import styles from "./index.module.less"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse, refreshTime } from "@/utils/utils"; import { useSelector, useDispatch } from "react-redux"; import { AlertFilled } from "@ant-design/icons"; import { OmpMessageModal } from "@/components"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import { getMaintenanceChangeAction } from "@/pages/SystemManagement/store/actionsCreators"; /*eslint-disable*/ //跟路由路径保持一致 const breadcrumbNameMap = { 404: "404", //"/":"仪表盘", homepage: "仪表盘", "resource-management": "资源管理", "machine-management": "主机管理", "system-settings": "系统管理", "user-management": "用户管理", "monitoring-settings": "监控设置", "system-management": "系统管理", "alarm-log": "告警记录", "exception-list": "异常清单", "application-monitoring": "应用监控", application_management: "应用管理", app_store: "应用商店", "app-service-detail": "服务详情", "app-component-detail": "组件详情", "status-patrol": "状态巡检", "patrol-inspection-record": "巡检记录", "patrol-strategy": "巡检策略", "status-patrol-detail": "分析报告", service_management: "服务管理", application_installation: "应用安装", component_installation: "组件安装", installation: "安装", "email-settings": "邮件管理", "rule-center": "指标中心", "default-rule": "默认指标", "install-record": "执行记录", service_upgrade: "服务升级", "deployment-plan": "部署模板", "data-backup": "数据备份", "backup-record": "备份记录", "operation-record": "操作记录", "login-log": "登录日志", "system-log": "系统记录", "fault-selfHealing":"故障自愈", "selfHealing-record":"自愈记录", "selfHealing-strategy":"自愈策略", "utilitie":"实用工具", "tool-management":"工具管理", "tool-management-detail": "工具详情", "task-record":"任务记录", "tool-execution-results":"执行结果", "indicator-rule": "指标规则", "extend-rule": "扩展指标" }; // 基于面包屑组件的一层封装,用于匹配当前路由地址,动态展示页面路径 const CustomBreadcrumb = withRouter(({ location, collapsed }) => { const dispatch = useDispatch(); const [loading, setLoading] = useState(false); //是否展示维护模式提示词 const time = useSelector((state) => state.customBreadcrumb.time); const isMaintenance = useSelector( (state) => state.systemManagement.isMaintenance ); const [closeMaintenanceModal, setCloseMaintenanceModal] = useState(false); //const appContext = useContext(context); //定义在首页时当前组件展示的时间 const [curentTime, setCurentTime] = useState(""); const pathSnippets = location.pathname; const extraBreadcrumbItems = (_, index) => { //console.log(pathSnippets); const url = pathSnippets.split("/"); //`/${pathSnippets.slice(0, index + 1).join("/")}` return ( <> {url.map((i, idx) => { if (idx == url.length - 3) { if (!breadcrumbNameMap[url[url.length - 2]]) { return ( {breadcrumbNameMap[i]} ); } } if (idx == url.length - 2) { // 动态路由的时候url的最后一项不一定能体现当前页面,也有可能是动态参数 if (!breadcrumbNameMap[url[url.length - 1]]) { return ( {breadcrumbNameMap[i]} ); } } if (idx == url.length - 1) { return ( {breadcrumbNameMap[i]} ); } else { return ( {breadcrumbNameMap[i]} ); } })} ); }; //在组件初始化时获取当前时间 useEffect(() => { //console.log(extraBreadcrumbItems); //因为在首页才会有时间展示,添加判断 dispatch(refreshTime()); }, []); // 更改维护模式 const changeMaintain = (e) => { setLoading(true); fetchPost(apiRequest.environment.queryMaintainState, { body: { matcher_name: "env", matcher_value: "default", }, }) .then((res) => { handleResponse(res, (res) => { //console.log(res) if (res.code == 0) { if (e) { message.success("已进入全局维护模式"); dispatch(getMaintenanceChangeAction(true)); } else { message.success("已退出全局维护模式"); dispatch(getMaintenanceChangeAction(false)); } } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setCloseMaintenanceModal(false); }); }; return (
{/*
*/} {extraBreadcrumbItems()} {/*
*/} {isMaintenance ? ( {" "} 当前处于维护模式, 退出维护模式请点击 { setCloseMaintenanceModal(true); }} style={{ color: "#2e7cee", cursor: "pointer" }} > {" "} 这里 ) : ( )} {/* */} {/* 刷新时间: {time} */} 提示 } loading={loading} onFinish={() => { changeMaintain(false); }} >
确定退出全局维护模式 ?
); }); export default React.memo(CustomBreadcrumb); /*eslint-disable*/ ================================================ FILE: omp_web/src/components/CustomBreadcrumb/index.module.less ================================================ .customNav { transition: all 0.2s ease-in-out; margin-top: 60px; position:fixed; z-Index:1000; //margin-left:200px; display: flex; align-items: center; justify-content: space-between; box-sizing: border-box; width: 100%; background-color: white; height: 45px; padding-left: 20px; padding-right: 20px; margin-bottom: 15px; font-size: 14px; //background-color: #fff; margin-left: 10px; border-left: 1px solid #e7e7e7; // /font-size: 16px; color: black; //padding-left: 20px; // fontWeight: 400, // color: "black", // paddingLeft: 20, //height: 52px; padding-top: 5px; padding-bottom: 5px; // display: "flex", // alignItems: "center", // borderBottom: "1px solid #dcdee5", & > div:last-child { margin-left: auto; } } .timeStampContainer { font-size: 13px; color: #8b8b8b; padding-right: 220px; // position: relative; // left: -100px; } ================================================ FILE: omp_web/src/components/CustomBreadcrumb/store/actionsCreators.js ================================================ import * as actionTypes from "./constants"; export const getMaintenanceChangeAction = (value) => ({ type: actionTypes.CHANGE_MAINTENANCE, payload: { isMaintenance:value } }); export const getRefreshTimeChangeAction = (value) => ({ type: actionTypes.CHANGE_REFRESHTIME, payload: { time:value } }); ================================================ FILE: omp_web/src/components/CustomBreadcrumb/store/constants.js ================================================ export const CHANGE_MAINTENANCE = "CHANGE_MAINTENANCE"; export const CHANGE_REFRESHTIME = "CHANGE_REFRESHTIME"; ================================================ FILE: omp_web/src/components/CustomBreadcrumb/store/index.js ================================================ import reducer from "./reduer"; export { reducer }; ================================================ FILE: omp_web/src/components/CustomBreadcrumb/store/reduer.js ================================================ import * as actionTypes from "./constants"; const defaultState = { isMaintenance:false }; function reducer(state = defaultState,action){ switch(action.type){ case actionTypes.CHANGE_MAINTENANCE: return {...state, isMaintenance: action.payload.isMaintenance}; case actionTypes.CHANGE_REFRESHTIME: return {...state, time: action.payload.time}; default: return state; } } export default reducer; ================================================ FILE: omp_web/src/components/OmpButton/index.js ================================================ import { Button } from "antd"; const OmpButton = (props) => { return ( ); }; useEffect(() => { console.log(searchTags); }, [searchTags]); return (
{operation}
{ let formData = formInstance.getFieldValue(); console.log(formData); onFinish(); }} >
{Object.keys(searchTags).length > 0 && (
{" "} 检索项 : {Object.keys(searchTags).map((key) => { return ( } onClose={(e) => { e.preventDefault(); let willDeleteItem = dictionary.filter( (item) => item.label == key ); formInstance.resetFields([willDeleteItem[0].name]); setSearchTags((tags) => { let newTags = { ...tags }; delete newTags[key]; return newTags; }); }} > {`${key}=${searchTags[key]}`} ); })}
)} setVisible(false)} visible={visible} width={560} bodyStyle={{ paddingLeft: 20, paddingRight: 20, }} >
{ let formData = formInstance.getFieldValue(); //console.log(formData,e,childrenArr) Object.keys(formData).map((i) => { let [info] = dictionary.filter((item) => item.name == i); // console.log(info,formData[i]) if (formData[i]) { let value = formData[i]; //如果是select,要展示text,根据value检索text if (info.children) { info.children.map((c) => { if (c.value == value) { value = c.children; } }); } setSearchTags((tags) => { return { ...tags, [info.label]: value, }; }); } }); setVisible(false); onFinish(); }} > {childrenArr.map((item) => { return (
{item.props.label}} key={item.props.label} name={item.props.label && item.props.name} labelAlign="right" style={{ width: `242px`, marginBottom: 15 }} > {item}
); })} {renderButtonGroup()}
); }; export default OmpCollapseWrapper; ================================================ FILE: omp_web/src/components/OmpCollapseWrapper/index.module.less ================================================ .OmpCollapseWrapper { width: 100%; padding-top: 5px; // background-color: #fff; // border: 1px solid rgb(220, 222, 229); // margin-top: 10px; // margin-bottom: 10px; .OmpCollapseFormWrapper { display: flex; position: relative; //left: -20px; left: 15px; //padding-left: 10px; > div { display: flex; flex-wrap: wrap; padding-bottom: 10px; >div { //width: 0px; padding-top: 10px; } } } } :global { .ant-picker-input { input { text-align: center; } } } ================================================ FILE: omp_web/src/components/OmpCollapseWrapper/indexOld.js ================================================ import { useState, useEffect, useLayoutEffect } from "react"; import styles from "./index.module.less"; import { Form, Input, Button } from "antd"; import { DownOutlined, } from "@ant-design/icons"; import { useSelector } from "react-redux"; const OmpCollapseWrapper = ({ children, onFinish, form, onReset, initialValues={} }) => { const [isExpand, setIsExpand] = useState(false) const [ defaultForm ] = Form.useForm(); const formInstance = form?form:defaultForm let childrenArr = Array.isArray(children) ? children : [children]; // 视口宽度 const viewWidth = useSelector(state => state.layouts.viewSize.width); //每个formitem 的初始值定为255 const [itemWidth, setItemWidth] = useState(255) // 计算当前组件的宽度 = 窗口宽度 - 左侧asideMenu宽度 - content区域的padding*2 let width = viewWidth - 240 - 20*2 // 计算在当前窗口宽度下,一行能放置几个formItem let num = (width - 240)/itemWidth let widthSurplus = (width - 240)%itemWidth/num //console.log(width,num,widthSurplus) useLayoutEffect(()=>{ // 根据剩余宽度重新计算每个foritem的宽度(目的是当剩余宽度过多,增加formitem宽度) setItemWidth((w)=>w + Number(widthSurplus) - 28) },[]) let result = childrenArr.slice(0,num) const renderButtonGroup = ()=>{ return (
{ childrenArr.length > num && setIsExpand(!isExpand)}> {isExpand?" 收起":" 展开"} }
) } return (
console.log(height)} className={styles.OmpCollapseWrapper} // / style={{height:60,overflow:"hidden"}} >
{(isExpand?childrenArr:result).map((item) => { return (
{item.props.label}} key={item.props.label} name={item.props.label && item.props.name} labelAlign="right" style={{width:item.props.width || `${itemWidth}px`}} > {item}
); })}
{renderButtonGroup()}
); }; export default OmpCollapseWrapper; ================================================ FILE: omp_web/src/components/OmpContentNav/index.js ================================================ import styles from "./index.module.less"; // 用于面包屑导航组件下部的 切换页面content的导航 const OmpContentNav = ({ data, currentFocus }) => { const focusedStyle = { color: "#4986f7", borderBottom: "2px solid #4986f7", //paddingBottom:10 height: "35px", marginRight: 15, zIndex: 2, }; return ( <>
{data.map((item, index) => { return (
item.handler()} > {item.text}
); })}
); }; export default OmpContentNav; ================================================ FILE: omp_web/src/components/OmpContentNav/index.module.less ================================================ .warningListHeader { display: flex; flex-flow: row nowrap; align-items: center; //border-bottom: 1px solid rgb(220, 222, 229); font-weight: 500; padding-top: 20px; //color: #333; //padding-bottom: 10px; //padding-right:35px; padding-left: 10px; & > div { height: 28px; padding: 0 10px; margin: 0 5px -1.5px; cursor: pointer; } } ================================================ FILE: omp_web/src/components/OmpContentWrapper/index.js ================================================ import styles from "./index.module.less"; function OmpContentWrapper({ children, wrapperStyle }) { return (
{children}
); } export default OmpContentWrapper; ================================================ FILE: omp_web/src/components/OmpContentWrapper/index.module.less ================================================ .contentWrapper { background-color: white; padding: 10px; padding-bottom: 30px; //height: 100%; // color: rgba(0,0,0,0.65); } ================================================ FILE: omp_web/src/components/OmpDatePicker/index.js ================================================ import { DatePicker } from "antd"; import moment from "moment"; const OmpDatePicker = ({...props}) => { return ( { return current && current >= moment().endOf("day"); }} showTime={{ hideDisabledOptions: true, }} //value={rangePickerValue} format="YYYY-MM-DD HH:mm:ss" {...props} // onChange={onChange} // onOk={(dates) => { // const start = moment(dates[0]).format("YYYY-MM-DD HH:mm:ss"); // const end = moment(dates[1]).format("YYYY-MM-DD HH:mm:ss"); // getServiceData(pagination, { // query_start_time: start, // query_end_time: end, // query_content: searchValue, // }); // }} /> ); }; export default OmpDatePicker; ================================================ FILE: omp_web/src/components/OmpDrawer/index.js ================================================ import { Drawer } from "antd"; import { DesktopOutlined } from "@ant-design/icons"; import { OmpIframe } from "@/components"; const OmpDrawer = ({ showIframe, setShowIframe }) => { return ( 信息面板 IP: {showIframe.record?.ip}
} headerStyle={{ padding:"19px 24px" }} placement="right" closable={true} width={`calc(100% - 200px)`} style={{ height: "calc(100%)", // paddingTop: "60px", }} onClose={() => { setShowIframe({ ...showIframe, isOpen: false, }); }} visible={showIframe.isOpen} bodyStyle={{ padding: 10, //paddingLeft:10, backgroundColor: "#e7e9f0", //"#f4f6f8" height: "calc(100%)", }} destroyOnClose={true} > ); }; export default OmpDrawer; ================================================ FILE: omp_web/src/components/OmpIframe/index.js ================================================ import { useEffect } from "react"; const OmpIframe = ({ showIframe, setShowIframe, iframeSrc }) => { useEffect(() => { let h = document.getElementById("root").clientHeight; document.getElementById("omp_iframe").style.height = h + "px"; document.getElementById("omp_iframe_container").style.height = h + "px"; }, []); let href = window.location.href.split("#")[0]; return ( <>
); }; export default OmpIframe; ================================================ FILE: omp_web/src/components/OmpMaintenanceModal/index.js ================================================ import { useState } from "react"; import { useDispatch } from "react-redux"; import { handleResponse } from "@/utils/utils"; import { fetchPut } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; const OmpMaintenanceModal = ({ control, used }) => { const dispatch = useDispatch(); const [isModalLoading, setIsModalLoading] = useState(false); const changeMaintenance = () => { setIsModalLoading(true); fetchPut(apiRequest.systemSettings.modeInfoChange, { body: { used: used, //env_id: Number(updata()().value), }, }) .then((res) => { res = res.data; handleResponse(res, () => { if (res.code === 0) { //dispatch(getMaintenanceChangeAction(res.data.used)); control[1](false); } }); }) .catch((e) => console.log(e)) .finally(() => { setIsModalLoading(false); }); }; return (
omp
// { // changeMaintenance(); // }} // > // {used?"当该环境处于维护模式时,该环境发出的警报将会被抑制。":"该环境退出维护模式,则从该环境内发出的警报将不会再被抑制。"} // ); }; export default OmpMaintenanceModal; ================================================ FILE: omp_web/src/components/OmpMessageModal/index.js ================================================ import { Modal, Button } from "antd"; const OmpMessageModal = ({ visibleHandle, children, title, onFinish, noFooter, loading = false, afterClose = () => {}, disabled = false, ...residualParam }) => { return ( } onCancel={() => visibleHandle[1](false)} footer={null} destroyOnClose loading={loading} afterClose={afterClose} > {children}
{noFooter ? ( "" ) : ( <> )}
); }; export default OmpMessageModal; ================================================ FILE: omp_web/src/components/OmpModal/index.js ================================================ import { Modal, Button, Form } from "antd"; import React from "react"; import styles from "./index.module.less"; const OmpModal = ({ visibleHandle, children, title, onFinish = () => {}, footer, loading = false, afterClose = () => {}, form, initialValues = {}, setLoading, okBtnText, beForeOk = () => {}, formLabelCol = { span: 7 }, formWrapperCol = { span: 14 }, ...residualParam }) => { const [modalForm] = Form.useForm(); // 扩展formItem功能,为了能够在formitem的validator校验时获得当前form的实例进行操作 // 在这里重写formitem的validator函数,在新函数中注入form实例 // console.log(children) let dealChild = Array.isArray(children) ? children : [children]; let processedChildren = dealChild?.map((item) => { if (item.props?.useforminstanceinvalidator === "true") { // 当前就是需要扩展validator的formItem // 拿到当前项的rules数组并把数组项为{validator:fn}的拿到,重写fn let newRules = item.props.rules.map((r) => { if (r.validator) { //重写validator return { validator: (rule, value, callback) => { return r.validator(rule, value, callback, modalForm); }, }; } else { return r; } }); return React.cloneElement(item, { rules: newRules }); } else if (item.props?.useforminstanceinonchange === "true") { let newOnChange = (e) => { item.props.onChange(e, modalForm); }; return React.cloneElement(item, { onChange: newOnChange }); } else { return item; } }); return ( onOk()} onCancel={() => visibleHandle[1](false)} footer={footer} destroyOnClose loading={loading} afterClose={() => { // 重置表单数据 form ? form.resetFields() : modalForm.resetFields(); //传入的afterClose afterClose(); }} footer={null} {...residualParam} >
{processedChildren} { beForeOk(); }} >
); }; export default OmpModal; ================================================ FILE: omp_web/src/components/OmpModal/index.module.less ================================================ :global { .ant-modal-header { padding-top: 12px; padding-bottom: 10px; } .ant-modal-title { font-size: 14px; font-weight: 500; } .ant-modal-close-x { line-height: 45px; } //修改form.item的垂直间距 .ant-form-item { margin-bottom: 18px; } //当弹出校验提示文字时,把marginbotton置成0 .ant-form-item-with-help { margin-bottom: 0px; } } ================================================ FILE: omp_web/src/components/OmpOperationWrapper/index.js ================================================ import styles from "./index.module.less" const OmpOperationWrapper = (props)=>{ return (
{props.children}
) } export default OmpOperationWrapper ================================================ FILE: omp_web/src/components/OmpOperationWrapper/index.module.less ================================================ .OmpOperationWrapper { //background-color: red; height: 54px; display: flex; align-items: center; justify-content: space-between; } ================================================ FILE: omp_web/src/components/OmpProgress/index.js ================================================ import ReactEcharts from "echarts-for-react"; const OmpProgress = ({ trafficWay, percent }) => { // 判断trafficWay的每一项是否都是0 let isAllNull = trafficWay.filter((i) => i.value !== 0); //console.log(isAllNull); if (isAllNull.length == 0) { trafficWay[2].value = 1; } var data = []; var color = [ "#ee686e", "#ffbe40", "rgb(84, 187, 166)", "#df7153", "#fad83a", "#c490bf", "#1fe15f", "#3087d6", "#4be1ff", ]; for (var i = 0; i < trafficWay.length; i++) { data.push( { value: trafficWay[i].value, name: trafficWay[i].name, itemStyle: { normal: { borderWidth: 4, //shadowBlur: 2, borderColor: color[i], //shadowColor: color[i], }, }, }, { value: 0, name: "", itemStyle: { normal: { label: { show: false, }, labelLine: { show: false, }, color: percent == 0 ? "#f45966":"rgba(0, 0, 0, 0)", borderColor: percent == 0 ? "#f45966":"rgba(0, 0, 0, 0)", borderWidth: 4, }, }, } ); } var seriesOption = [ { name: "", type: "pie", clockWise: false, radius: ["75%", "77%"], center: ["50%", "50%"], hoverAnimation: false, itemStyle: { normal: { label: { show: false, position: "outside", color: "#ddd", // formatter: function (params) { // var percent = 0; // var total = 0; // for (var i = 0; i < trafficWay.length; i++) { // total += trafficWay[i].value; // } // percent = ((params.value / total) * 100).toFixed(0); // if (params.name !== "") { // return ( // "交通方式:" + // params.name + // "\n" + // "\n" + // "占百分比:" + // percent + // "%" // ); // } else { // return ""; // } // }, }, labelLine: { length: 30, length2: 100, show: false, color: "#00ffff", }, }, }, data: data, }, ]; const option = { // backgroundColor: '#0A2E5D', color: color, title: { text: `${percent == Infinity ? 100 : isNaN(percent) ? 0 : percent}%`, top: "37%", left: "47%", textAlign: "center", textStyle: { color: `${percent == Infinity ? 100 : isNaN(percent) ? 0 : percent}%` == "100%" ? "rgb(84, 187, 166)" : "rgba(0, 0, 0, 0.65)", fontSize: 18, fontWeight: "400", }, }, graphic: { elements: [ { type: "image", z: 3, style: { // image: img, width: 178, height: 178, }, left: "center", top: "center", position: [100, 100], }, ], }, tooltip: { show: false, }, // legend: { // icon: "circle", // orient: "vertical", // // x: 'left', // data: [ // "物理机", // "宿主机", // "云主机", // "网络设备", // "安全设备", // "应用系统", // "存储设备", // "网络服务", // "终端PC", // ], // right: "5%", // top: "center", // align: "left", // textStyle: { // color: "black", // }, // itemGap: 20, // // formatter: function(name) { // // let target,percent; // // for (let i = 0; i < dataPie.length; i++) { // // if (dataPie[i].name === name) { // // target = dataPie[i].value; // // percent = ((target/total)*100).toFixed(2); // // } // // } // // let arr = [ percent+'% '+' {yellow|' + target + '}', ' {blue|' + name + '}' ]; // // return arr.join("\n") // // } // }, toolbox: { show: false, }, series: seriesOption, }; return ( ); }; export default OmpProgress; ================================================ FILE: omp_web/src/components/OmpSelect/index.js ================================================ import { Select } from "antd"; import { useState, useRef, useEffect } from "react"; const OmpSelect = ({ searchLoading, selectValue, listSource, setSelectValue, fetchData, ...props }) => { const [searchValue, setSearchValue] = useState(""); //select 的onblur函数拿不到最新的search value,使用useref存(是最新的,但是因为失去焦点时会自动触发清空search,还是得使用ref存) const searchValueRef = useRef(null); useEffect(()=>{ if(!selectValue){ searchValueRef.current = ""; setSearchValue(); } },[selectValue]) return ( ); }; export default OmpSelect; ================================================ FILE: omp_web/src/components/OmpStateBlock/index.js ================================================ import { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import { Button, Checkbox, Popover } from "antd"; import styles from "./index.module.less"; const colorObj = { normal: { background: "#eefaf4", borderColor: "#54bba6", }, abnormal: { background: "#fbe7e6", borderColor: "#da4e48", }, noMonitored: { background: "#e5e5e5", borderColor: "#aaaaaa", }, warning: { background: "rgba(247, 231, 24, 0.2)", borderColor: "rgb(245, 199, 115)", }, }; function OmpStateBlock(props) { const history = useHistory(); const { title, data = [] } = props; const [allData, setAllData] = useState([]); //console.log(data) const [currentData, setCurrentData] = useState([]); const [isShowAll, setIsShowAll] = useState(false); useEffect(() => { if (data && data.length > 0) { let handleData = data.map((item) => { //后端的critical和warning都渲染成红色,在前端对数据进行处理 noMonitored:"未监控" abnormal:"异常" normal:"正常" // let status = "noMonitored"; // if (item.severity == "warning" || item.severity == "critical") { // status = "abnormal"; // } else if (item.severity == "normal") { // status = "normal"; // } // return { // ...item, // frontendStatus: status, // }; let status = "noMonitored"; if (item.severity == "critical") { status = "abnormal"; } else if (item.severity == "normal") { status = "normal"; } else if (item.severity == "warning") { status = "warning"; } return { ...item, frontendStatus: status, }; }); setCurrentData(sortData(handleData)); setAllData(sortData(handleData)); } }, [data]); const sortData = (data) => { let normalArr = data.filter((i) => i.frontendStatus == "normal"); let noMonitoredArr = data.filter((i) => i.frontendStatus == "noMonitored"); let abnormalArr = data.filter((i) => i.frontendStatus == "abnormal"); let warningArr = data.filter((i) => i.frontendStatus == "warning"); let result = abnormalArr .concat(normalArr) .concat(noMonitoredArr) .concat(warningArr); return result; }; return (
{title}
{ if (e.target.checked) { setCurrentData( sortData( currentData.concat( allData.filter( (i) => i.frontendStatus == "abnormal" || i.frontendStatus == "warning" ) ) ) ); } else { setCurrentData( sortData( currentData.filter( (i) => i.frontendStatus !== "abnormal" || i.frontendStatus == "warning" ) ) ); } }} > 异常 { if (e.target.checked) { setCurrentData( sortData( currentData.concat( allData.filter((i) => i.frontendStatus == "normal") ) ) ); } else { setCurrentData( sortData( currentData.filter((i) => i.frontendStatus !== "normal") ) ); } }} > 正常 { if (e.target.checked) { setCurrentData( sortData( currentData.concat( allData.filter((i) => i.frontendStatus == "noMonitored") ) ) ); } else { setCurrentData( sortData( currentData.filter( (i) => i.frontendStatus !== "noMonitored" ) ) ); } }} > 未监控
{currentData.length > 0 ? (
{currentData.map((item, idx) => { return (
{ item.frontendStatus == "abnormal" ? props.criticalLink(item) : props.link(item); }} >
); })}
) : (
暂无数据
)}
); } export default OmpStateBlock; function popContent(item) { return (
{item.info.map((i) => (
{i.ip && ( {i.ip} )} {i.date ? ( {i.date} ) : ( {item.frontendStatus === "noMonitored" ? "未监控" : "正常"} )} {i.describe && ( {i.describe.length > 100 ? i.describe.slice(0, 100) + "..." : i.describe.slice(0, 100)} )}
))}
); } ================================================ FILE: omp_web/src/components/OmpStateBlock/index.module.less ================================================ .homepageWrapper { padding: 15px; .pageBlock { .blockTitle { font-weight: 500; //color: #333; margin-bottom: 10px; } } } .blockContent { //border: 1px solid #DCDEE5; background-color: #fff; border-radius: 5px; padding: 10px; margin-bottom: 15px; } .blockOverviewItem { flex: 1; display: flex; justify-content: center; flex-flow: row nowrap; padding: 10px; margin-right: 20px; border-radius: 5px; & > div:nth-child(1) { margin-right: 15px; align-self: center; & > div:nth-child(1) { width: 80px !important; height: 80px !important; font-size: 16px !important; } } .progressInfo { color: #333; & > div:nth-child(1) { font-size: 14px; font-weight: 500; margin-bottom: 15px; } & > div:nth-child(2) { margin-bottom: 10px; } } } .checkboxGroup { display: flex; justify-content: space-between; width: 100%; margin-bottom: 10px; .blockTitle { font-weight: 500; } div { display: flex; } } .blockItemWrapper { display: flex; flex-flow: row wrap; padding-top: 5px; //justify-content: space-between; } // .blockItemWrapper>div:last-child { // flex:1 // } .stateButton:hover { top: -2px; box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3); color: black !important; // color: white; // /* 设置字体阴影 */ // text-shadow: 1px 1px 2px #35cac2; // /* 改变按钮边界 */ // border: 1px solid #35cac2; /* 设置按钮阴影 */ //box-shadow: 5px 5px 50px rgba(255, 255, 255, 0.4) inset; } .stateButton { position: relative; top: 0px; color: rgba(0, 0, 0, 0.65); transition: all 0.2s ease-in-out; width: 174px; margin-right: 10px; margin-bottom: 10px; font-size: 13px; & > div { max-width: 145px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } .popContent { & > span { font-size: 13px; margin-right: 15px; } .ip { //display: inline-flex; // todo 这个地方最大宽度不确定,此处为ip地址最大长度,但会导致ip地址较短时页面空一大块 // /min-width: 60px; } } .dropBtn { height: 24px; font-size: 12px; // position: relative; // top: -2px; } .emptyTable { background-color: #fff; border-radius: 5px; padding: 10px; margin-bottom: 15px; height: 20px; font-size: 12px; color: rgba(0, 0, 0, 0.25); line-height: 0px; text-align: center; //overflow-y: scroll } ================================================ FILE: omp_web/src/components/OmpTable/components/OmpTableFilter.js ================================================ import { Dropdown, Menu } from "antd"; import { FilterFilled } from "@ant-design/icons"; import { useState } from "react"; const OmpTableFilter = ({ dataIndex, filterMenuList, queryRequest, initfilter }) => { // 表格的筛选控制器 const [filterControl, setFilterControl] = useState(initfilter); // 展开菜单是否 const [dropDownIsOpen, setDropDownIsOpen] = useState(false); return ( { if (filterControl) { queryRequest({ [dataIndex]: null }); setFilterControl(""); } else { setDropDownIsOpen(e); } }} overlay={ { setFilterControl(e.key); queryRequest({ [dataIndex]: e.key }); setDropDownIsOpen(false); }} > {filterMenuList?.map((item) => { return {item.text}; })} } placement="bottomCenter" trigger="click" > ); }; export default OmpTableFilter; ================================================ FILE: omp_web/src/components/OmpTable/index.js ================================================ import { Table, Tree } from "antd"; import styles from "./index.module.less"; import { useLayoutEffect, useState } from "react"; import { useSelector } from "react-redux"; import OmpTableFilter from "./components/OmpTableFilter"; import { SettingOutlined } from "@ant-design/icons"; const OmpTable = ({ checkedState, columns, notSelectable, noScroll, ...residualParam }) => { const [checkedList, setCheckedList] = checkedState ? checkedState : []; // 视口高度 const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 视口宽度 const viewWidth = useSelector((state) => state.layouts.viewSize.width); const [maxWidth, setMaxWidth] = useState(1900); // 表格项的筛选selectKey const [selectKeys, setSelectKeys] = useState( columns.map((item) => item.dataIndex) ); // 当columns传入usefilter时,对该项做处理 const extensionsColumns = columns.map((item, idx) => { // // 复制一下item // let item = R.clone(i); // // 当columns的width未设置时,直接添加width200,然后计算最大宽度 // if (!item.width) { // item = { // width: 120, // ellipsis: true, // ...item, // }; // } //item.isShow = true; let lastIndex = columns.length - 1; if (idx == lastIndex) { return { ...item, filterIcon: (filtered) => , filterDropdown: ({ confirm, clearFilters }) => (
} selectable={false} onCheck={(checkedKeys, info) => { setSelectKeys(checkedKeys); }} treeData={columns.map((item, idx) => { return { ...item, disabled: idx == columns.length - 1 ? true : false, }; })} checkedKeys={selectKeys} />
), }; } // 筛选功能 if (item.usefilter) { return { ...item, filterIcon: () => { return ( ); }, filters: [{ text: "mock", value: "mock" }], filterDropdown: () => { return ; }, }; } return { ...item, }; }); // 计算表格实际横向宽度(最大) // useLayoutEffect(() => { // let maxW = 0 // extensionsColumns.map((item) => { // maxW += item.width // }); // console.log(maxW) // setMaxWidth(maxW) // //console.log(extensionsColumns) // }, []); // useEffect(()=>{ // },[selectKeys]) useLayoutEffect(() => { //console.log(viewHeight); // 为了能够让omptable能够根据视口高度进行自适应 // 订出如下标准 视口高度大于955 设置 表格cell的padding为1rem // 视口高度大于 760 设置cell的padding为0.72rem let cellPadding = ".5"; if (viewHeight > 955) { cellPadding = ".9"; } else if (viewHeight <= 955 && viewHeight > 860) { cellPadding = ".75"; } else if (viewHeight <= 860 && viewHeight > 760) { cellPadding = ".6"; } try { //window.style = "body{background-color:blue;}"; var stylee = document.createElement("style"); stylee.type = "text/css"; var sHtml = ` .ant-table-thead > tr > th, .ant-table-tbody > tr > td, .ant-table tfoot > tr > th, .ant-table tfoot > tr > td { padding: ${cellPadding}rem; }`; stylee.innerHTML = sHtml; document.getElementsByTagName("head").item(0).appendChild(stylee); } catch (error) { console.log(error); } }, []); return ( maxWidth ? null : { x: (maxWidth + 30) }} scroll={viewWidth > 1900 ? null : { x: noScroll ? null : 1500 }} {...residualParam} columns={extensionsColumns.filter((i) => { return selectKeys.includes(i.dataIndex); })} //size="small" rowSelection={ checkedState && { onSelect: (record, selected, selectedRows) => { if (selected) { setCheckedList([...checkedList, record]); } else { setCheckedList(checkedList.filter((m) => m.id !== record.id)); } }, onSelectAll: (selected, selectedRows, changeRows) => { if (selected) { setCheckedList([...checkedList, ...changeRows]); } else { setCheckedList((ls) => { return ls.filter((l) => { let ids = changeRows.map((m) => m.id); return !ids.includes(l.id); }); }); } }, getCheckboxProps: notSelectable || ((record) => ({ disabled: record.is_read === 1, })), selectedRowKeys: checkedList.map((item) => item?.id), // 传入rowselect优先使用传入的 ...residualParam.rowSelection, } } /> ); }; export default OmpTable; ================================================ FILE: omp_web/src/components/OmpTable/index.module.less ================================================ .OmpTableWrapper { //background-color: red; // .OmpTableRow { // background-color: aqua !important; // } //margin-bottom: 100px; } :global { .ant-table-small .ant-table-thead > tr > th { background-color: #fafbfd; } // .ant-table { // color:rgba(0, 0, 0, 0.65); // } // .ant-badge { // color:rgba(0, 0, 0, 0.65); // } .ant-table-tbody > tr.ant-table-row-selected > td { border-color:rgba(0, 0, 0, 0.03); background-color:#fff } table tr th.ant-table-selection-column, table tr td.ant-table-selection-column { padding-left: 15px; } .ant-table-content { // border-top: 1px solid rgb(220, 222, 229)!important; } // 之后即使是全局样式也配置在相应的组件里 .ant-table-wrapper { //border: 1px solid #dcdee5; } .ant-table-body { overflow-y: auto !important; } .ant-table-header { border-bottom: 1px dashed #c4c6cc; } .ant-table-thead { color: #313238; font-weight: 400; font-size: 12px; tr > th { background-color: #fafbfd; background-color: #eaedf4; background-color: rgba(73,134,247,0.1); background-color: #fafbfd; border-bottom: 1px solid rgb(220, 222, 229)!important; color: rgba(0,0,0,0.65); } // .ant-table-cell :after { // background-color: red; // } } .ant-table-tbody { font-size: 12px; } .ant-table-pagination.ant-pagination { //border-top: 1px dashed #c4c6cc; margin: 0px; } .ant-table-pagination-right { padding: 12px; font-size: 12px; background-color: #fff; } .ant-pagination-options-size-changer { font-size: 12px; } // .ant-table.ant-table-middle .ant-table-title, // .ant-table.ant-table-middle .ant-table-footer, // .ant-table.ant-table-middle .ant-table-thead > tr > th, // .ant-table.ant-table-middle .ant-table-tbody > tr > td, // .ant-table.ant-table-middle tfoot > tr > th, // .ant-table.ant-table-middle tfoot > tr > td { // padding: 1rem; // } .ant-select-item-option-content{ //font-size: 12px !important; } //表格排序按钮紧贴title .ant-table-column-sorters { display:inline-flex; // align-items: center; .ant-table-column-title{ position: relative; top:1px; left: -3px; } .ant-table-column-sorter { position: relative; left: 3px; } // .ant-table-column-sorter-full {} } //表格筛选按钮紧贴title .ant-table-filter-column { display:inline-flex; .ant-table-column-title{ position: relative; top:1px; left: -4px; } .ant-dropdown-trigger { position: relative; left: 0px; //top:1px } //是为了解决过滤图标和选中背景生效范围不一致问题 .ant-table-filter-trigger { padding: 0px; margin: 0; position: relative; left: 3px; } } } .warningSearch { display: flex; justify-content: flex-end; margin-top: 10px; margin-bottom: 10px; & > div:nth-child(1) { margin-right: auto; } .rangePicker { margin-right: 15px; } } .contentLeftMenuWrapper { display: flex; .leftMenu { background-color: #fafafa; padding: 15px; margin-top: 10px; margin-right: 10px; height: 82vh; overflow-y: auto; } .leftMenuListContent { display: flex; flex-direction: column; font-size: 16px; // label :nth-child(1){ // padding-top:5px // } label { font-size: 16px!important; padding-top: 10px; } :global { .ant-checkbox-group-item { //margin-bottom: 7px; font-size: 14px!important; } .ant-checkbox-checked { position: relative; top: 1px; } } } } .trendContentWrapper { width: calc(100% - 0px); padding: 10px; .title { margin-bottom: 5px; margin-left: 15px; color: #333; font-size: 16px; font-weight: 500; padding: 10px 0; } .trendItemContent { background-color: #fff; border-radius: 10px; .header { display: flex; align-items: center; padding: 8px 8px 8px 15px; color: #333; border-bottom: 1px solid #bfbfbf; & > div:nth-child(2) { display: flex; align-items: center; width: 100%; height: 35px; margin-left: 5px; } } .rangPicker { display: flex; justify-content: flex-end; width: 100%; padding: 10px 60px 10px; } } } .pageInfo { display: flex; justify-content: flex-end; align-items: center; padding: 10px; & > div:nth-child(1) { margin-right: 10px; } } .panelItem { background: #fafafa; border-radius: 4px; border: 0; overflow: hidden; border-bottom: 0 !important; & > div:nth-child(1) { padding: 8px 15px; } // 覆盖展开后的content背景色 & > div:nth-child(2) { background-color: #fff !important; } } // .reportContentWrapper { // } .reportContent { padding: 15px; .reportTitle { display: flex; justify-content: center; color: #1890ff; margin-bottom: 20px; height: 40px; align-items: center; position: relative; & > div:nth-child(1) { font-size: 22px; font-weight: 500; color: #333; } & > div:nth-child(2) { position: absolute; right: 0; display: flex; cursor: pointer; & > div:nth-child(1) { margin-right: 10px; } } } } .overviewItemWrapper { display: flex; justify-content: space-between; flex-flow: wrap; margin: 10px 0; & > div:nth-child(even) { margin-right: 0; } } .overviewItem { display: flex; width: 49.5%; color: #333; margin-bottom: 10px; & > div { border: 1px solid #E8E8E8; padding: 10px; } & > div:nth-child(1) { display: flex; justify-content: center; align-items: center; border-right: none; width: 200px; } & > div:nth-child(2) { width: 100%; } } .planChartWrapper { margin-top: 10px; width: 100%; border:1px solid #E8E8E8; border-radius: 2px; // /padding:10px; .planChartTitle { background-color: #fafbfd; font-weight: 500; height: 30px; line-height: 30px; border-bottom: solid 1px #E8E8E8; .planChartTitleCircular { display: inline-block; width: 10px; height: 10px; background-color: #54bba6; border-radius: 50%; margin-right: 10px; margin-left: 20px; } } .planChartBlockWrapper { display: flex; flex-flow: row wrap; max-height: 240px; overflow-y: auto; padding-top:15px; padding:20px; .stateButton { position: relative; top: 0px; //background-color: #eefaf4; border:1px solid #333; transition: all .2s ease-in-out; width: 178px; margin-right: 32px; margin-bottom: 10px; height: 32px; & > div { margin: auto; width: 100%; height: 100%; line-height: 30px; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } } } .topologyWrapper { display: flex; align-items: center; margin-right: 80px; margin-bottom: 80px; .topologyChildren { display: flex; flex-direction: column; & > div:nth-child(1) { position: relative; .verticalLine { position: absolute; background-color: #333; left: 0; top: 22px; height: calc(100% - 55px); width: 2px; border-radius: 1px; } } } .topologyItem { display: flex; justify-content: center; align-items: center; padding: 10px; border: 2px solid #333; border-radius: 5px; } .rootItemBox { display: flex; align-items: center; margin-bottom: 10px; .topologyTitle { } } .connectLine { display: inline-block; width: 30px; background-color: #333; height: 2px; border-radius: 1px; } } // .expandedRowWrapper { // width: 100% ; // } .basicCardWrapper { display: flex; flex-flow: row wrap; .basicCardItem { width: 40%; padding: 5px; margin-right: 10px; margin-bottom: 10px; } } .buttonContainer { width: 100%; .greenType { background-color: #5ba165; color: #fff; border-color: #5ba165; } .redType { background-color: #ff4d4f; color: #fff; border-color: #ff4d4f; } & > button { margin-right: 15px; } } ._bigfontSize{ font-size: 14px; } ================================================ FILE: omp_web/src/components/OmpToolTip/index.js ================================================ import { Tooltip } from "antd"; const OmpToolTip = ({ children, maxLength, ...props }) => { children = children || "-"; if (children.length > maxLength) { return ( {children.substring(0, maxLength)}... ); } else { return children; } }; export default OmpToolTip; ================================================ FILE: omp_web/src/components/index.js ================================================ import OmpContentNav from "./OmpContentNav"; import OmpContentWrapper from "./OmpContentWrapper"; import OmpOperationWrapper from "./OmpOperationWrapper"; import OmpTable from "./OmpTable"; import OmpModal from "./OmpModal"; import OmpCollapseWrapper from "./OmpCollapseWrapper"; import OmpStateBlock from "./OmpStateBlock"; import CustomBreadcrumb from "./CustomBreadcrumb"; // import OmpEnvSelect from "./OmpEnvSelect"; import OmpDatePicker from "./OmpDatePicker"; import OmpButton from "./OmpButton"; import OmpMessageModal from "./OmpMessageModal"; import OmpProgress from "./OmpProgress"; import OmpIframe from "./OmpIframe"; import OmpSelect from "./OmpSelect"; import OmpDrawer from "./OmpDrawer"; import OmpToolTip from "./OmpToolTip"; export { OmpContentNav, OmpContentWrapper, OmpOperationWrapper, OmpTable, OmpModal, OmpCollapseWrapper, OmpStateBlock, CustomBreadcrumb, // OmpEnvSelect, OmpDatePicker, OmpButton, OmpMessageModal, OmpProgress, OmpIframe, OmpSelect, OmpDrawer, OmpToolTip, }; ================================================ FILE: omp_web/src/config/requestApi.js ================================================ export const apiRequest = { environment: { //环境数据查询 //queryEnvList: "/api/v1/env" // 查询全局维护模式 queryMaintainState: "/api/promemonitor/globalMaintain/", }, auth: { // 用户认证 login: "/api/login/", // 登入 // 首次请求用于验证登出 users: "/api/users/users/", // 修改密码 changePassword: "/api/users/updatePassword/", }, homepage: { instrumentPanel: "/api/promemonitor/instrumentPanel/", }, machineManagement: { // 主机管理 hosts: "/api/hosts/hosts/", ipList: "/api/hosts/ips/", // 主机详情 hostDetail: "/api/hosts/hostsDetail/", // 主机名和ip地址校验接口 checkHost: "/api/hosts/fields/", operateLog: "/api/hosts/operateLog/", // 重启主机agent restartHostAgent: "/api/hosts/restartHostAgent/", // 重启监控agent restartMonitorAgent: "/api/promemonitor/restartMonitorAgent/", // 重装主机agent reInstallHostAgent: "/api/hosts/hostReinstall/", // 重装监控agent reInstallMonitorAgent: "/api/hosts/monitorReinstall/", // 主机进入退出维护模式 hostsMaintain: "/api/hosts/maintain/", // 主机初始化脚本下载地址 downInitScript: "/api/hosts/hostInit/", // 主机初始化 hostInit: "/api/hosts/hostInit/", // 主机agent状态查询 hostsAgentStatus: "/api/hosts/hostsAgentStatus/", // 主机批量导入模版下载地址 downTemplate: "/api/hosts/batchValidate/", // 主机批量导入文件解析后的校验接口(post) batchValidate: "/api/hosts/batchValidate/", // 主机批量导入创建主机 batchImport: "/api/hosts/batchImport/", // 主机删除 deleteHost: "/api/hosts/hostUninstall/", }, MonitoringSettings: { // 配置初始查询监控 monitorurl: "/api/promemonitor/monitorurl/", // 修改配置监控 multiple_update: "/api/promemonitor/monitorurl/multiple_update/", // 查询推送配置 queryPushConfig: "/api/promemonitor/getSendAlertSetting/", // 更新邮件配置 updatePushConfig: "/api/promemonitor/updateSendAlertSetting/", }, Alert: { // 告警记录页面查询 listAlert: "/api/promemonitor/listAlert/", // 告警记录已读 updateAlert: "/api/promemonitor/updateAlert/", // 告警记录筛选实例名称 instanceNameList: "/api/promemonitor/instanceNameList/", }, ExceptionList: { // 异常清单列表查询 exceptionList: "/api/promemonitor/grafanaurl/", }, appStore: { // 组件查询 queryComponents: "/api/appStore/components/", // 服务查询 queryServices: "/api/appStore/services/", // 服务筛选条件列表 queryLabels: "/api/appStore/labels/", // 基础组件详情 ProductDetail: "/api/appStore/componentDetail/", // 应用服务详情 ApplicationDetail: "/api/appStore/serviceDetail/", // 安装包删除 remove: "/api/appStore/remove/", // 安装包校验结果 pack_verification_results: "/api/appStore/pack_verification_results/", // 发布命令下发 publish: "/api/appStore/publish/", // 扫描服务端命令下发 executeLocalPackageScan: "/api/appStore/executeLocalPackageScan/", // 扫描状态查询 localPackageScanResult: "/api/appStore/localPackageScanResult/", // 服务列表 services: "/api/services/services/", // 服务详情查询 servicesDetail: "/api/services/services", // 服务的操作 servicesAction: "/api/services/action/", // 服务的删除提示信息 servicesDeleteMsg: "/api/services/delete/", // 模版下载 applicationTemplate: "/api/appStore/applicationTemplate/", // 组件安装查询接口 componentEntrance: "/api/appStore/componentEntrance/", // 产品安装查询接口 productEntrance: "/api/appStore/productEntrance/", // 服务安装校验 executeInstall: "/api/appStore/executeInstall/", // 服务校验通过后的状态查询接口 installHistory: "/api/appStore/installHistory/", // 服务查询安装记录 serviceInstallHistoryDetail: "/api/appStore/serviceInstallHistoryDetail/", // 服务批量安装选择应用服务列表 queryBatchInstallationServiceList: "/api/appStore/batchInstallEntrance/", // 服务批量安装选择应用确认操作 createInstallInfo: "/api/appStore/createInstallInfo/", // 批量安装 checkInstallInfo: "/api/appStore/checkInstallInfo/", // 服务分布数据查询 createServiceDistribution: "/api/appStore/createServiceDistribution/", // 服务分布已安装服务查询 queryListServiceByIp: "/api/appStore/listServiceByIp/", // 服务批量安装服务分布确认操作 checkServiceDistribution: "/api/appStore/checkServiceDistribution/", // 服务批量安装修改配置ip初始查询 getInstallHostRange: "/api/appStore/getInstallHostRange/", // 服务批量安装根据ip查询安装相应参数 getInstallArgsByIp: "/api/appStore/getInstallArgsByIp/", // 服务批量安装开始安装操作 createInstallPlan: "/api/appStore/createInstallPlan/", // 批量安装-开始安装信息查询 queryInstallProcess: "/api/appStore/showInstallProcess", // 批量安装-服务安装进度详情查询 showSingleServiceInstallLog: "/api/appStore/showSingleServiceInstallLog/", // 批量安装-组件安装操作下发 createComponentInstallInfo: "/api/appStore/createComponentInstallInfo/", // 批量安装-安装重试操作 retryInstall: "/api/appStore/retryInstall/", // 服务升级选择应用服务列表 canUpgrade: "/api/upgrade/can-upgrade", // 服务升级动作下发 doUpgrade: "/api/upgrade/do-upgrade", // 服务升级页面列表进度查询 queryUpgradeProcess: "/api/upgrade/history", // 服务回退选择应用服务列表 canRollback: "/api/rollback/can-rollback", // 服务回退动作下发 doRollback: "/api/rollback/do-rollback", // 服务回退页面列表进度查询 queryRollbackProcess: "/api/rollback/history", // 删除应用商店 deleteServer: "/api/appStore/delete/", // 获取可纳管服务列表 queryAppList: "/api/services/appList/", // 校验服务纳管配置信息 appConfCheck: "/api/services/appConfCheck/", }, installHistoryPage: { queryInstallHistoryList: "/api/appStore/mainInstallHistory", queryUpgradeHistoryList: "/api/upgrade/history", queryRollbackHistoryList: "/api/rollback/history", queryAllList: "/api/appStore/executionRecord/", }, inspection: { inspectionList: "/api/inspection/history/", // 巡检记录详情 reportDetail: "/api/inspection/report", // 巡检策略查询 queryPatrolStrategy: "/api/inspection/crontab/0/", // 创建巡检任务 createPatrolStrategy: "/api/inspection/crontab/", // 修改巡检任务 updatePatrolStrategy: "/api/inspection/crontab/0/", // 深度分析|主机巡检|组件巡检 任务下发 taskDistribution: "/api/inspection/history/", // 巡检渲染组件列表 servicesList: "/api/inspection/services/", // 巡检导出 download: "/download-inspection", // 查询推送配置 queryPushConfig: "/api/inspection/inspectionSendEmailSetting/", // 更新邮件配置 updatePushConfig: "/api/inspection/inspectionSendEmailSetting/", // 推送邮件 pushEmail: "/api/inspection/inspectionSendEmail/", }, emailSetting: { // 查询邮件全局设置 querySetting: "/api/promemonitor/getSendEmailConfig/", // 更新邮件全局设置 updateSetting: "/api/promemonitor/updateSendEmailConfig/", }, // 指标中心 ruleCenter: { // 主机指标 hostThreshold: "/api/promemonitor/hostThreshold/", // 服务指标 serviceThreshold: "/api/promemonitor/serviceThreshold/", // 定制化指标 queryCustomThreshold: "/api/promemonitor/customThreshold/", // 请求指标规则列表 queryPromemonitor: "/api/promemonitor/quota/", // 内置指标 queryBuiltinsQuota: "/api/promemonitor/builtinRule/", // 添加,修改规则 addQuota: "/api/promemonitor/quota/", // 测试 testPromSql: "/api/promemonitor/testPromSql/", // 删除 deleteQuota: "/api/promemonitor/quota/", // 启动,停用 batchUpdateRule: "/api/promemonitor/batchUpdateRule/", // 扩展指标列表查询 queryExtendRuleList: "/api/promemonitor/customScript/", // 扩展指标详情查询 queryDetail: "/api/promemonitor/customScriptJobInfo/", }, // 部署计划 deloymentPlan: { // 服务验证 serviceValidate: "/api/appStore/deploymentPlanValidate/", // 部署计划导入 serviceImport: "/api/appStore/deploymentPlanImport/", // 部署计划列表 deploymentList: "/api/appStore/deploymentPlanList/", // 部署是否可用 deploymentOperable: "/api/appStore/deploymentOperable", // 下载部署计划模板 deploymentTemplate: "/api/appStore/deploymentTemplate/", }, // 数据备份 dataBackup: { // 数据库备份策略 strategySetting: "api/backups/backupSettings/", // 自定义参数 backupCustom: "api/backups/backupCustom/", // 可备份实例 queryCanBackup: "api/backups/canBackupInstances/", // 查询自定义参数是否存在实例使用 backupRepeatCustom: "api/backups/backupRepeatCustom/", // 备份记录 queryBackupHistory: "api/backups/backupHistory/", }, operationRecord: { // 登录日志 queryLoginLog: "/api/users/UserLoginLog/", // 系统记录 querySystemLog: "/api/users/operateLog/", }, faultSelfHealing: { // 自愈记录 querySelfHealingList: "/api/services/ListSelfHealingHistory/", // "/api/services/ListSelfHealingHistory/", // 自愈已读 selfHeadlingIsRead: "/api/services/UpdateSelfHealingHistory/", // 自愈策略 selfHealingStrategy: "/api/services/SelfHealingSetting/", }, // 实用工具 utilitie: { queryList: "/api/tool/toolList/", queryFormConf: "/api/tool/form/", uploadFile: "/api/common/upload_file/", queryResult: "/api/tool/result/", queryHistory: "/api/tool/execute-history", queryBuiltinsQuota: "/api/promemonitor/builtinRule/", }, }; ================================================ FILE: omp_web/src/config/router.config.js ================================================ import AppStore from "@/pages/AppStore"; import AppStoreDetail from "@/pages/AppStore/config/detail"; //import VersionManagement from "@/pages/ProductsManagement/VersionManagement"; import MachineManagement from "@/pages/MachineManagement"; import UserManagement from "@/pages/UserManagement"; import MonitoringSettings from "@/pages/MonitoringSettings"; import SystemManagement from "@/pages/SystemManagement"; import AlarmLog from "@/pages/AlarmLog"; import ExceptionList from "@/pages/ExceptionList"; import PatrolInspectionRecord from "@/pages/PatrolInspectionRecord"; import PatrolStrategy from "@/pages/PatrolStrategy"; import PatrolInspectionDetail from "@/pages/PatrolInspectionRecord/config/detail"; import ServiceManagement from "@/pages/ServiceManagement"; import ComponentInstallation from "@/pages/AppStore/config/ComponentInstallation"; import ApplicationInstallation from "@/pages/AppStore/config/ApplicationInstallation"; import Installation from "@/pages/AppStore/config/Installation"; import EmailSettings from "src/pages/EmailSettings"; import RuleCenter from "src/pages/RuleCenter"; import InstallationRecord from "@/pages/InstallationRecord"; import Upgrade from "@/pages/AppStore/config/Upgrade"; import Rollback from "@/pages/AppStore/config/Rollback"; import DeploymentPlan from "@/pages/DeploymentPlan"; import BackupRecords from "@/pages/BackupRecords"; import BackupStrategy from "@/pages/BackupStrategy"; import LoginLog from "@/pages/LoginLog"; import SystemLog from "@/pages/SystemLog"; import SelfHealingRecord from "@/pages/SelfHealingRecord"; import SelfHealingStrategy from "@/pages/SelfHealingStrategy"; import ToolManagement from "@/pages/ToolManagement"; import TaskRecord from "@/pages/TaskRecord"; import ToolDetails from "@/pages/ToolManagement/detail"; import ToolExecution from "@/pages/ToolExecution"; import ToolExecutionResults from "@/pages/ToolExecutionResults"; import RuleIndicator from "@/pages/RuleIndicator"; import RuleExtend from "@/pages/RuleExtend"; import GetService from "@/pages/AppStore/config/GetService"; import { DesktopOutlined, ClusterOutlined, ProfileOutlined, SettingOutlined, LineChartOutlined, AppstoreOutlined, EyeOutlined, UnorderedListOutlined, SaveOutlined, SolutionOutlined, InteractionOutlined, ToolOutlined, } from "@ant-design/icons"; export default [ { menuTitle: "资源管理", menuIcon: , menuKey: "/resource-management", children: [ { title: "主机管理", path: "/resource-management/machine-management", component: MachineManagement, }, ], }, { menuTitle: "应用管理", menuIcon: , menuKey: "/application_management", children: [ { title: "服务管理", path: "/application_management/service_management", component: ServiceManagement, }, { title: "应用商店", path: "/application_management/app_store", component: AppStore, }, { title: "组件安装", path: "/application_management/app_store/component_installation/:name", notInMenu: true, component: ComponentInstallation, }, { title: "应用安装", path: "/application_management/app_store/application_installation/:name", notInMenu: true, component: ApplicationInstallation, }, { title: "应用商店服务详情", path: "/application_management/app_store/app-service-detail/:name/:verson", notInMenu: true, component: AppStoreDetail, }, { title: "应用商店组件详情", path: "/application_management/app_store/app-component-detail/:name/:verson", notInMenu: true, component: AppStoreDetail, }, { title: "批量安装", path: "/application_management/app_store/installation", notInMenu: true, component: Installation, }, // { // title: "服务安装", // path: "/application_management/app_store/installation-service", // notInMenu: true, // component: Installation, // }, { title: "执行记录", path: "/application_management/install-record", component: InstallationRecord, }, { title: "服务升级", path: "/application_management/app_store/service_upgrade", notInMenu: true, component: Upgrade, }, { title: "服务回滚", path: "/application_management/app_store/service_rollback", notInMenu: true, component: Rollback, }, { title: "部署模板", path: "/application_management/deployment-plan", component: DeploymentPlan, }, { title: "服务纳管", path: "/application_management/get-service", notInMenu: true, component: GetService, }, ], }, { menuTitle: "应用监控", menuIcon: , menuKey: "/application-monitoring", children: [ { title: "异常清单", path: "/application-monitoring/exception-list", component: ExceptionList, }, { title: "告警记录", path: "/application-monitoring/alarm-log", component: AlarmLog, }, { title: "监控设置", path: "/application-monitoring/monitoring-settings", component: MonitoringSettings, }, ], }, { menuTitle: "故障自愈", menuIcon: , menuKey: "/fault-selfHealing", children: [ { title: "自愈策略", path: "/fault-selfHealing/selfHealing-strategy", component: SelfHealingStrategy, }, { title: "自愈记录", path: "/fault-selfHealing/selfHealing-record", component: SelfHealingRecord, }, ], }, { menuTitle: "状态巡检", menuIcon: , menuKey: "/status-patrol", children: [ { title: "巡检记录", path: "/status-patrol/patrol-inspection-record", component: PatrolInspectionRecord, }, { title: "巡检记录详情", path: "/status-patrol/patrol-inspection-record/status-patrol-detail/:id", notInMenu: true, component: PatrolInspectionDetail, }, { title: "巡检策略", path: "/status-patrol/patrol-strategy", component: PatrolStrategy, }, ], }, { menuTitle: "指标中心", menuIcon: , menuKey: "/rule-center", children: [ // { // title: "默认指标", // path: "/rule-center/default-rule", // component: RuleCenter, // }, { title: "指标规则", path: "/rule-center/indicator-rule", component: RuleIndicator, }, { title: "扩展指标", path: "/rule-center/extend-rule", component: RuleExtend, }, ], }, { menuTitle: "数据备份", menuIcon: , menuKey: "/data-backup", children: [ { title: "备份策略", path: "/data-backup/backup-strategy", component: BackupStrategy, }, { title: "备份记录", path: "/data-backup/backup-record", component: BackupRecords, }, ], }, { menuTitle: "实用工具", menuIcon: , menuKey: "/utilitie", children: [ { title: "工具管理", path: "/utilitie/tool-management", component: ToolManagement, }, { title: "工具详情", path: "/utilitie/tool-management/tool-management-detail/:id", notInMenu: true, component: ToolDetails, }, { title: "工具执行", path: "/utilitie/tool-management/tool-execution/:id", notInMenu: true, component: ToolExecution, }, { title: "执行结果", path: "/utilitie/tool-management/tool-execution-results/:id", notInMenu: true, component: ToolExecutionResults, }, { title: "任务记录", path: "/utilitie/task-record", component: TaskRecord, }, ], }, { menuTitle: "操作记录", menuIcon: , menuKey: "/operation-record", children: [ { title: "登录日志", path: "/operation-record/login-log", component: LoginLog, }, { title: "系统记录", path: "/operation-record/system-log", component: SystemLog, }, ], }, { menuTitle: "系统设置", menuIcon: , menuKey: "/system-settings", children: [ { title: "用户管理", path: "/system-settings/user-management", component: UserManagement, }, { title: "系统管理", path: "/system-settings/system-management", component: SystemManagement, }, { title: "邮件管理", path: "/system-settings/email-settings", component: EmailSettings, }, ], }, ]; ================================================ FILE: omp_web/src/index.js ================================================ import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( , document.getElementById('root') ); ================================================ FILE: omp_web/src/layouts/container/container.js ================================================ import React, { useEffect, useState } from "react"; const Container = ({ children, ...arg }) => { let extendChild = React.cloneElement(children, arg); return (
{extendChild}
); }; export default Container; ================================================ FILE: omp_web/src/layouts/index.js ================================================ import { Layout, Menu, Dropdown, message, Form, Input } from "antd"; import { MenuUnfoldOutlined, MenuFoldOutlined, DashboardOutlined, CaretDownOutlined, QuestionCircleOutlined, CaretUpOutlined, } from "@ant-design/icons"; import React, { useState, useEffect, useRef } from "react"; import img from "@/config/logo/logo.svg"; import styles from "./index.module.less"; import routerConfig from "@/config/router.config"; import { useHistory, useLocation } from "react-router-dom"; import { CustomBreadcrumb, OmpModal } from "@/components"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse, _idxInit, logout, isPassword, encrypt, } from "@/utils/utils"; import { useDispatch } from "react-redux"; import { getSetViewSizeAction } from "./store/actionsCreators"; import { getMaintenanceChangeAction } from "@/pages/SystemManagement/store/actionsCreators"; const { Header, Content, Footer, Sider } = Layout; const { SubMenu } = Menu; const OmpLayout = (props) => { const reduxDispatch = useDispatch(); const history = useHistory(); const location = useLocation(); //不可用状态是一个全局状态,放在layout const [disabled, setDisabled] = useState(false); const [isLoading, setLoading] = useState(false); const [collapsed, setCollapsed] = useState(false); const rootSubmenuKeys = [ "/machine-management", "/products-management", "/operation-management", "/actions-record", "/product-settings", "/system-settings", ]; const headerLink = [ // { title: "仪表盘", path: "/homepage" }, // { title: "应用商店", path: "/application_management/app_store" }, { title: "快速部署", path: "/application_management/deployment-plan" }, // { title: "数据上传", path: "/products-management/version/upload" }, // { title: "深度分析", path: "/operation-management/report" }, { title: "监控平台", path: "/proxy/v1/grafana/d/XrwAXz_Mz/mian-ban-lie-biao", }, ]; const [currentOpenedKeys, setCurrentOpenedKeys] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); //修改密码弹框 const [showModal, setShowModal] = useState(false); //用户相关信息 const [userInfo, setUserInfo] = useState({}); const menu = ( setShowModal(true)}> 修改密码 logout()}> 退出登录 ); const toggle = () => { setCollapsed(!collapsed); }; const onPathChange = (e) => { //console.log(e); if (e.key === history.location.pathname) { return; } // homepage没有submenu if (e.key === "/homepage") { setCurrentOpenedKeys([]); } history.push(e.key); }; const onOpenChange = (openKeys) => { const latestOpenKey = openKeys.find( (key) => currentOpenedKeys.indexOf(key) === -1 ); //console.log(latestOpenKey) if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) { setCurrentOpenedKeys(openKeys); } else { setCurrentOpenedKeys(latestOpenKey ? [latestOpenKey] : []); } }; const onPassWordChange = (data) => { setLoading(true); fetchPost(apiRequest.auth.changePassword, { body: { username: encrypt(localStorage.getItem("username")), old_password: encrypt(data.old_password), new_password: encrypt(data.new_password2), }, }) .then((res) => { handleResponse(res, (res) => { console.log(res); if (res.code == 0) { message.success("修改密码成功, 请重新登录"); setShowModal(false); setTimeout(() => { logout(); }, 1000); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 相应路由跳转,submenu打开 useEffect(() => { try { let pathArr = location.pathname.split("/"); console.log(pathArr); if (pathArr[1] == "homepage") { setSelectedKeys(["/homepage"]); } else { setSelectedKeys([`/${pathArr[1]}/${pathArr[2]}`]); } let newPath = `/${pathArr[1]}`; setCurrentOpenedKeys([newPath]); } catch (e) { console.log(e); } }, [location]); useEffect(() => { window.__history__ = history; fetchGet(apiRequest.auth.users) .then((res) => { if (res && res.data.code == 1 && res.data.message == "未认证") { } res.data && res.data.data && setUserInfo(res.data.data[0]); }) .catch((e) => { console.log(e); }) .finally(() => setLoading(false)); }, []); const antiShakeRef = useRef(null); const getViewSize = () => { reduxDispatch( // 这里做一个视口查询,存入store, 其他组件可以根据视口大小进行自适应 getSetViewSizeAction({ height: document.documentElement.clientHeight, width: document.documentElement.clientWidth, }) ); }; getViewSize(); useEffect(() => { window.onresize = () => { if (!antiShakeRef.current) { antiShakeRef.current = true; setTimeout(() => { getViewSize(); antiShakeRef.current = false; }, 300); } }; }, []); // 防止在校验进入死循环 const flag = useRef(null); // 查询全局维护模式状态 const queryMaintainState = () => { fetchGet(apiRequest.environment.queryMaintainState) .then((res) => { handleResponse(res, (res) => { //console.log(res) if (res.data) { reduxDispatch(getMaintenanceChangeAction(res.data.length !== 0)); } }); }) .catch((e) => { console.log(e); }) .finally(); }; useEffect(() => { queryMaintainState(); }, []); return ( : } collapsible collapsed={collapsed} onCollapse={toggle} collapsedWidth={50} >
{!collapsed && (
history.push("/homepage")} > {/* 运维管理平台 */} 运维工具包
)}
{ if (e.isOpen) { return ; } else { return ( ); } }} > }> 仪表盘 {routerConfig.map((item) => { return ( {item.children.map((i) => { if (!i.notInMenu) { return {i.title}; } })} ); })}
{headerLink.map((item, idx) => { return (
{ if (!disabled || item.title === "快速部署") { if (item.title === "监控平台") { window.open( "/proxy/v1/grafana/d/XrwAXz_Mz/mian-ban-lie-biao" ); } else { history.push(item.path); } } }} > {item.title}
); })}
{localStorage.getItem("username")}{" "}
版本信息:V1.1 } >
{props.children}
Copyright © 2020-2023 Cloudwise.All Rights Reserved{" "}
{ flag.current = true; }} afterClose={() => { flag.current = null; }} > { if (value) { if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } return Promise.resolve("success"); } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} > { if (value) { if (!flag.current) { passwordModalForm.validateFields(["new_password2"]); } if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } return Promise.resolve("success"); } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} > { if (value) { if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } if ( passwordModalForm.getFieldValue().new_password1 === value || !value ) { return Promise.resolve("success"); } else { return Promise.reject("两次密码输入不一致"); } } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} >
); }; export default OmpLayout; ================================================ FILE: omp_web/src/layouts/index.module.less ================================================ .headerLogo { width: 60px; display: flex; height: 100%; align-items: center; justify-content: center; & > img:nth-child(1) { width: 30px; height: 30px; } } .trigge { padding: 0 24px; font-size: 18px; line-height: 64px; cursor: pointer; transition: color 0.3s; } .OmpLayoutContainer { color: #63656e; height: 100%; //height: 58px; //background-color: red; .siteLayoutBackground { border-right: solid 1px #dcdee5; //padding-left:20px; //padding-top:0px ; } .OmpHeader { display: flex; align-items: center; width: 100%; height: 58px; color: white; font-size: 14px; white-space: nowrap; background-color: #1a2131; .headerSelect { width: 160px; } .headerDropDown { font-size: 14px; margin-left: 20px; cursor: pointer; padding: 7px 10px; padding-right: 90px; border: 1px solid #979aa4; .headerDropDownIcon { position: relative; left: 70px; } } .envMenuWrapper { margin-left: auto; } .headerLogo { width: 60px; display: flex; height: 100%; align-items: center; justify-content: center; & > img:nth-child(1) { width: 30px; height: 30px; } } div:nth-child(2) { margin-right: 75px; font-size: 16px; font-weight: 500; } .userAvatar { width: 100px; //margin-right: 70px; margin-left: auto; display: flex; align-items: center; //width: 100%; //justify-content: flex-end; position: relative; top: 1px; } .userAvatarIcon { position: relative; top: 1px; left: 3px; } } .MenuTop { height: 52px; background-color: white; border-bottom: 1px solid #dcdee5; display: flex; justify-content: center; align-items: center; } // .siteLayoutBackground { // background-color: red; // } } .headerLink { display: flex; align-items: center; justify-content: center; padding: 0 20px; color: #8d919c; color: rgba(255, 255, 255, 0.75); font-weight: 500; font-size: 15px; cursor: pointer; position: relative; top:1px } .headerLinkNohover { display: flex; align-items: center; justify-content: center; padding: 0 15px; color: #8d919c; color: rgba(255, 255, 255, 0.75); font-weight: 500; font-size: 15px; cursor: pointer; } .headerLink:hover { background: #121925; } :global { .ant-layout-sider-has-trigger { position: fixed; height: 100%; } .ant-pagination { color: rgba(0, 0, 0, 0.65) !important; } .ant-btn { color: rgba(0, 0, 0, 0.65); } .ant-btn-primary { color: #fff; } .ant-menu { color: rgba(0, 0, 0, 0.65); } .ant-select { color: rgba(0, 0, 0, 0.65) !important; } .site-layout .site-layout-background { // background: #fff; background-color: #161c25; color: white; height: 60px; } #components-layout-demo-custom-trigger .trigger { padding: 0 24px; font-size: 18px; line-height: 64px; cursor: pointer; transition: color 0.3s; } #components-layout-demo-custom-trigger .trigger:hover { color: #1890ff; } #components-layout-demo-custom-trigger .logo { height: 32px; margin: 16px; background: rgba(255, 255, 255, 0.3); } .ant-layout-sider-trigger { background: #fff; color: black; font-size: 18px; // border-right: 1px solid #f0f0f0 } .ant-layout-sider-children { background: #151a21; } // .site-layout .site-layout-background { // background: #fff; // height: 50px; // } // 修改antd menu的原生选中样式 // 选中文字的颜色 // .ant-menu.ant-menu-dark, .ant-menu-dark .ant-menu-sub, .ant-menu.ant-menu-dark .ant-menu-sub { // background-color: #192031; // color: #fff; // } // .ant-layout-sider { // background-color: #2e4254; // } // .ant-menu-dark.ant-menu-dark:not(.ant-menu-horizontal) .ant-menu-item-selected { // background-color: #4a6782; // color: #36bebc; // } // .ant-menu-dark .ant-menu-item:hover, .ant-menu-dark .ant-menu-item-active, .ant-menu-dark .ant-menu-submenu-active, .ant-menu-dark .ant-menu-submenu-open, .ant-menu-dark .ant-menu-submenu-selected, .ant-menu-dark .ant-menu-submenu-title:hover { // color: #36bebc; // } // .ant-menu-dark .ant-menu-item:hover > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-item-active > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-submenu-title:hover > .ant-menu-submenu-title > .ant-menu-submenu-arrow::after, .ant-menu-dark .ant-menu-item:hover > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before, .ant-menu-dark .ant-menu-item-active > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before, .ant-menu-dark .ant-menu-submenu-active > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before, .ant-menu-dark .ant-menu-submenu-open > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before, .ant-menu-dark .ant-menu-submenu-selected > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before, .ant-menu-dark .ant-menu-submenu-title:hover > .ant-menu-submenu-title > .ant-menu-submenu-arrow::before { // background-color: #36bebc; // } // .ant-menu-dark .ant-menu-item, .ant-menu-dark .ant-menu-item-group-title, .ant-menu-dark .ant-menu-item > a, .ant-menu-dark .ant-menu-item > span > a { // color: #fff // } // .ant-menu-dark .ant-menu-item-selected .ant-menu-item-icon, .ant-menu-dark .ant-menu-item-selected .anticon { // color: #36bebc; // } // .ant-menu-dark .ant-menu-item-selected .ant-menu-item-icon + span, .ant-menu-dark .ant-menu-item-selected .anticon + span { // color: #36bebc; // } // .ant-menu.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow, .ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow, .ant-menu.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow { // opacity: 1; // } .ant-spin-nested-loading { //height: 100%; width: 100%; } .ant-spin-container { //height: 100%; width: 100%; } // search 的搜索 .ant-input-search > .ant-input-group > .ant-input-group-addon:last-child .ant-input-search-button { padding-top: 1px; } // 按钮高度统一调整 .ant-btn { padding: 4px 20px; border-radius: 4px; } .ant-btn-sm { padding: 1px 7px; } // 翻页组件样式修改 // .ant-pagination-prev, // .ant-pagination-next, // .ant-pagination-total-text, // .ant-pagination-jump-prev, // .ant-pagination-jump-next { // height: 30px !important; // min-width: 30px !important; // line-height: 30px !important; // } // .ant-pagination-item { // height: 30px !important; // min-width: 30px !important; // line-height: 30px !important; // } // .ant-pagination-options { // .ant-select-selector { // height: 30px !important; // min-width: 30px !important; // line-height: 30px !important; // } // } .ant-menu-inline, .ant-menu-vertical, .ant-menu-vertical-left { border-right: none; } // .ant-table-pagination { // color: red!important; // } // .ant-menu-vertical .ant-menu-item::after, .ant-menu-vertical-left .ant-menu-item::after, .ant-menu-vertical-right .ant-menu-item::after, .ant-menu-inline .ant-menu-item::after { // border-right:3px solid #4986f7 // } .ant-menu-inline .ant-menu-item, .ant-menu-inline .ant-menu-submenu-title { width: 100%; } } ================================================ FILE: omp_web/src/layouts/indexOld.js ================================================ import { Layout, Menu, Dropdown, message, Form, Input, Button } from "antd"; import { MenuUnfoldOutlined, MenuFoldOutlined, DashboardOutlined, CaretDownOutlined, QuestionCircleOutlined, } from "@ant-design/icons"; import React, { useState, useEffect } from "react"; import img from "@/config/logo/logo.svg"; import styles from "./index.module.less"; import routerConfig from "@/config/router.config"; import { useHistory, useLocation } from "react-router-dom"; import { CustomBreadcrumb, OmpModal } from "@/components"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse, _idxInit, logout } from "@/utils/utils"; const { Header, Content, Footer, Sider } = Layout; const { SubMenu } = Menu; const OmpLayout = (props) => { const history = useHistory(); const location = useLocation(); //不可用状态是一个全局状态,放在layout const [disabled, setDisabled] = useState(false); const [isLoading, setLoading] = useState(false); const [collapsed, setCollapsed] = useState(false); const rootSubmenuKeys = [ "/machine-management", "/products-management", "/operation-management", "/actions-record", "/product-settings", "/system-settings", ]; const menu = ( setShowModal(true)}> 修改密码 logout()}> 退出登录 ); const [currentOpenedKeys, setCurrentOpenedKeys] = useState([]); const [selectedKeys, setSelectedKeys] = useState([]); //修改密码弹框 const [showModal, setShowModal] = useState(false); //用户相关信息 const [userInfo, setUserInfo] = useState({}) const toggle = () => { setCollapsed(!collapsed); }; const onPathChange = (e) => { console.log(e); if (e.key === history.location.pathname) { return; } // homepage没有submenu if (e.key === "/homepage") { setCurrentOpenedKeys([]); } history.push(e.key); }; const onOpenChange = (openKeys) => { const latestOpenKey = openKeys.find( (key) => currentOpenedKeys.indexOf(key) === -1 ); //console.log(latestOpenKey) if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) { setCurrentOpenedKeys(openKeys); } else { setCurrentOpenedKeys(latestOpenKey ? [latestOpenKey] : []); } }; const onPassWordChange = (data) => { setLoading(true); fetchPost(apiRequest.auth.password, { body: { ...data, }, }) .then((res) => { handleResponse(res); if (res.code == 0) { setShowModal(false); logout(); } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 相应路由跳转,submenu打开 useEffect(() => { try { if ( location.pathname == "/products-management/version/rapidDeployment" || location.pathname == "/products-management/version/upload" || location.pathname == "/products-management/version/installationDetails" ) { setSelectedKeys(["/products-management/version"]); } else { setSelectedKeys([location.pathname]); } let pathArr = location.pathname.split("/"); let newPath = `/${pathArr[1]}`; setCurrentOpenedKeys([newPath]); } catch (e) { console.log(e); } }, [location]); useEffect(() => { window.__history__ = history; fetchGet(apiRequest.auth.users) .then((res) => { if (res && res.data.code == 1 && res.data.message == "未认证") { //message.warning("登录失效,请重新登录") //history.replace("/login"); } console.log(res.data) res.data && setUserInfo(res.data.data[0]) }) .catch((e) => { console.log(e); }) .finally(() => setLoading(false)); }, []); return (
{!collapsed && (
history.push("/homepage")} > 运维管理平台
)}
}> 仪表盘 {routerConfig.map((item) => { return ( {item.children.map((i) => { return {i.title}; })} ); })}
{React.createElement( collapsed ? MenuUnfoldOutlined : MenuFoldOutlined, { style: { padding: "0 24px", fontSize: "18px", lineHeight: "60px", cursor: "pointer", transition: "color 0.3s", }, onClick: toggle, } )}
{userInfo?.username}{" "}
版本信息:V0.1.0 } >
{props.children}
Copyright © 2020-2021 Cloudwise.All Rights Reserved{" "}
{ if ( passwordModalForm.getFieldValue().new_password1 === value || !value ) { return Promise.resolve("success"); } else { return Promise.reject("两次密码输入不一致"); } }, }, ]} >
); }; export default OmpLayout; ================================================ FILE: omp_web/src/layouts/store/actionsCreators.js ================================================ import * as actionTypes from "./constants"; export const getSetViewSizeAction = (viewSize) => ({ type: actionTypes.SET_VIEWSIZE, payload: { viewSize:viewSize } }); ================================================ FILE: omp_web/src/layouts/store/constants.js ================================================ export const SET_VIEWSIZE = "SET_VIEWSIZE"; //export const CHANGE_ENVINFO = "CHANGE_ENVINFO"; ================================================ FILE: omp_web/src/layouts/store/index.js ================================================ import reducer from "./reduer"; export { reducer }; ================================================ FILE: omp_web/src/layouts/store/reduer.js ================================================ import * as actionTypes from "./constants"; const defaultState = { viewSize:{ height:0, width:0 }, a:"1" }; function reducer(state = defaultState,action){ switch(action.type){ case actionTypes.SET_VIEWSIZE: return {...state, viewSize: action.payload.viewSize}; case "c": return {...state, a: "123"}; default: return state; } } export default reducer; ================================================ FILE: omp_web/src/pages/AlarmLog/config/columns.js ================================================ import { colorConfig } from "@/utils/utils"; import { Tooltip, Badge } from "antd"; import moment from "moment"; const getColumnsConfig = ( queryRequest, setShowIframe, updateAlertRead, history ) => { return [ { title: "实例名称", key: "alert_instance_name", dataIndex: "alert_instance_name", align: "center", width: 200, ellipsis: true, fixed: "left", // sorter: (a, b) => a.alert_instance_name - b.alert_instance_name, // sortDirections: ["descend", "ascend"], render: (text, record) => { return ( {record.alert_instance_name ? record.alert_instance_name : "-"} ); }, }, { title: "IP地址", key: "alert_host_ip", width: 200, dataIndex: "alert_host_ip", ellipsis: true, sorter: (a, b) => a.alert_host_ip - b.alert_host_ip, sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { return ( { text && history.push({ pathname: "/resource-management/machine-management", state: { ip: text, }, }); }} > {text} ); }, }, { title: "级别", key: "alert_level", dataIndex: "alert_level", align: "center", width: 120, // sorter: (a, b) => a.severity - b.severity, // sortDirections: ["descend", "ascend"], //ellipsis: true, //width:120, usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "critical", text: "严重", }, { value: "warning", text: "警告", }, ], render: (text) => { switch (text) { case "critical": return 严重; case "warning": return 警告; default: return "-"; } }, }, { title: "告警类型", key: "alert_type", dataIndex: "alert_type", // usefilter: true, // queryRequest: queryRequest, // filterMenuList: [ // { // value: "service", // text: "服务", // }, // { // value: "host", // text: "主机", // }, // ], align: "center", //ellipsis: true, width: 150, render: (text) => { if (text == "host") { return "主机"; } else if (text == "service") { return "服务"; } else if (text == "component") { return "组件"; } else if (text == "database") { return "数据库"; } }, }, { title: "告警描述", key: "alert_describe", dataIndex: "alert_describe", align: "center", width: 420, ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "告警时间", width: 180, key: "alert_time", dataIndex: "alert_time", align: "center", //ellipsis: true, sorter: (a, b) => a.alert_time - b.alert_time, sortDirections: ["descend", "ascend"], render: (text) => { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; }, }, { title: "更新时间", width: 180, key: "create_time", dataIndex: "create_time", align: "center", //ellipsis: true, // sorter: (a, b) => a.create_time - b.create_time, // sortDirections: ["descend", "ascend"], render: (text) => { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; }, }, { title: "操作", width: 100, key: "", dataIndex: "", fixed: "right", align: "center", render: function renderFunc(text, record, index) { //console.log(record); return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/AlarmLog/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpSelect, OmpDatePicker, OmpDrawer, } from "@/components"; import { Button, Select, message, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import getColumnsConfig from "./config/columns"; import { SearchOutlined } from "@ant-design/icons"; import moment from "moment"; import { useHistory, useLocation } from "react-router-dom"; const AlarmLog = () => { const history = useHistory(); const location = useLocation(); const initTime = location.state?.time; const [time, setTime] = useState([]); const initIp = location.state?.ip; const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //选中的数据 const [checkedList, setCheckedList] = useState([]); //table表格数据 const [dataSource, setDataSource] = useState([]); const [ipListSource, setIpListSource] = useState([]); const [selectValue, setSelectValue] = useState(initIp); const [instanceSelectValue, setInstanceSelectValue] = useState( location.state?.alert_instance_name ); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 筛选label const [labelControl, setLabelControl] = useState( initIp ? "ip" : "instance_name" ); const [showIframe, setShowIframe] = useState({}); function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams = {}, ordering ) { setLoading(true); fetchGet(apiRequest.Alert.listAlert, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchIPlist(); //fetchNameList(); }); } const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; const updateAlertRead = (ids = []) => { setLoading(true); fetchPost(apiRequest.Alert.updateAlert, { body: { ids: ids, is_read: 1, }, }) .then((res) => { handleResponse(res, (res) => { message.success("已读成功"); }); }) .catch((e) => console.log(e)) .finally(() => { setCheckedList([]); setLoading(false); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ...pagination.searchParams }, pagination.ordering ); }); }; useEffect(() => { if (initTime) { setTime([moment(initTime), moment(initTime)]); fetchData(pagination, { alert_host_ip: location.state?.ip, alert_instance_name: location.state?.alert_instance_name, start_alert_time: moment(initTime).format("YYYY-MM-DD HH:mm:ss"), end_alert_time: moment(initTime).format("YYYY-MM-DD HH:mm:ss"), }); } else { let currentTime = moment(); let aMonthAgoTime = moment().subtract(1, "months"); console.log(currentTime, aMonthAgoTime); setTime([aMonthAgoTime, currentTime]); fetchData(pagination, { alert_host_ip: location.state?.ip, alert_instance_name: location.state?.alert_instance_name, start_alert_time: aMonthAgoTime.format("YYYY-MM-DD HH:mm:ss"), end_alert_time: currentTime.format("YYYY-MM-DD HH:mm:ss"), }); } }, []); return (
{ if (!e) { setTime([]); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, start_alert_time: null, end_alert_time: null, }, pagination.ordering ); } else { let result = e.filter((item) => item); if (result.length == 2) { setTime([moment(e[0]), moment(e[1])]); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, start_alert_time: moment(e[0]).format( "YYYY-MM-DD HH:mm:ss" ), end_alert_time: moment(e[1]).format( "YYYY-MM-DD HH:mm:ss" ), }, pagination.ordering ); } } }} value={time} />
{labelControl === "ip" && ( { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert_host_ip: value }, pagination.ordering ); }} pagination={pagination} /> )} {labelControl === "instance_name" && ( { setInstanceSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert_instance_name: null, }, pagination.ordering ); } }} onBlur={() => { if (instanceSelectValue) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert_instance_name: instanceSelectValue, }, pagination.ordering ); } }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert_instance_name: instanceSelectValue, }, pagination.ordering ); }} suffix={ !instanceSelectValue && ( ) } /> )}
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig( (params) => { // console.log(pagination.searchParams) fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, ...params }, pagination.ordering ); }, setShowIframe, updateAlertRead, history )} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
); }; export default AlarmLog; ================================================ FILE: omp_web/src/pages/AppStore/config/ApplicationInstallation.js ================================================ import { useEffect, useState, useRef } from "react"; import { useHistory } from "react-router-dom"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { Steps, Form, Input, Button, Select, Tooltip, message } from "antd"; import { LeftOutlined, DownOutlined, InfoCircleOutlined, LoadingOutlined, } from "@ant-design/icons"; import styles from "./index.module.less"; import RenderComDependence from "./component/RenderComDependence"; import { useDispatch } from "react-redux"; import { getTabKeyChangeAction } from "../store/actionsCreators"; const step2Open = (num) => ({ marginTop: 10, minHeight: 30, height: num * 75, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }); const step2NotOpen = () => ({ height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }); const step3Open = () => ({ marginTop: 10, padding: 10, minHeight: 30, height: 240, transition: "all .2s ease-in", overflow: "hidden", color: "#fff", backgroundColor: "#222222", wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "pre-line", overflowY: "auto", overflowX: "hidden", }); const step3NotOpen = () => ({ height: 0, minHeight: 0, padding: 0, transition: "all .2s ease-in", overflow: "hidden", color: "#fff", backgroundColor: "#222222", wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "pre-line", overflowY: "auto", overflowX: "hidden", }); const ApplicationInstallation = () => { const [form] = Form.useForm(); const dispatch = useDispatch(); const [processContinue, setProcessContinue] = useState(true); //定义校验弹出msessage函数 const verificationError = (arr) => { // console.log(arr); for (const item of arr) { if (item.process_continue == false) { setProcessContinue(false); message.warning(item.process_message); return false; break; } } setProcessContinue(true); return true; }; const history = useHistory(); let pathArr = history.location.pathname.split("/"); let name = pathArr[pathArr.length - 1]; const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [stepNum, setStepNum] = useState(0); // setp2的查看更多配置是否是展开状态 const [isOpen, setIsOpen] = useState({ [name]: false, }); // step3的安装详情是否是展开状态 因为多个所以为对象 const [isDetailOpen, setIsDetailOpen] = useState({}); const [versionCurrent, setVersionCurrent] = useState(""); const [step1Data, setStep1Data] = useState({}); const [step2Data, setStep2Data] = useState({}); const [step3Data, setStep3Data] = useState({}); // 第二步校验通过后,存储数据 const [vPassedresData, setVPassedresData] = useState({}); // const containerRef = useRef(null); const timer = useRef(null); function fetchData() { setLoading(true); fetchGet(apiRequest.appStore.productEntrance, { params: { pro_name: name, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res.data); setDataSource(res.data); // 设置版本默认选中第一个 //console.log(form); verificationError(res.data[0]?.pro_services); setVersionCurrent(res.data[0]?.pro_version); form.setFieldsValue({ version: res.data[0]?.pro_version }); form.setFieldsValue({ clusterMode: "single", }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } let currentproDependenceData = dataSource.filter( (item) => item.pro_version == versionCurrent )[0]; const [ipListSource, setIpListSource] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const use_exist_servicesRef = useRef([]); const install_servicesRef = useRef([]); // 定义选中的ip是否包含在instance_info const hasSameIp = (item, ip) => { return ( item.is_base_env && item.instance_info && item.instance_info.filter((b) => b.ip == ip) ); }; const [showJdk, setShowJdk] = useState(false); const jdkHandleInitData = (data = [], ip) => { console.log("jdk 尝试设置默认值", data); //data.map((item) => { if (hasSameIp(data, ip).length == 0) { console.log("不存在相同"); setShowJdk(true); install_servicesRef.current = [ { ...data, }, ]; use_exist_servicesRef.current = []; data?.app_install_args?.map((i) => { setIsOpen({ ...isOpen, [i.name]: false, }); form.setFieldsValue({ [`install|${data.name}|${JSON.stringify({ name: i.name, key: i.key, dir_key: i.dir_key, })}`]: i.default, }); }); data?.app_port?.map((i) => { setIsOpen({ ...isOpen, [i.name]: false, }); form.setFieldsValue({ [`port|${data.name}|${JSON.stringify({ name: i.name, key: i.key, //dir_key: i.dir_key, })}`]: i.default, }); }); form.setFieldsValue({ [`${data.name}|ip`]: ip, [`${data.name}|instanceName`]: `${data.name}-${ip[ip.length - 2]}-${ ip[ip.length - 1] }`, }); } else { setShowJdk(false); //console.log("存在相同"); let isSame = hasSameIp(data, ip)[0]; use_exist_servicesRef.current = [isSame]; install_servicesRef.current = []; } //}); }; const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); const firstIP = res.data[0].split("."); form.setFieldsValue({ ip: res.data[0], instanceName: `${name}-${firstIP[firstIP.length - 2]}-${ firstIP[firstIP.length - 1] }`, }); let firstIp = res.data[0]; // jdk 数据默认设置 jdkHandleInitData( currentproDependenceData?.dependence_services_info[0], firstIp ); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; // 开始安装get const queryInstallationInfo = (operateId) => { fetchGet(apiRequest.appStore.installHistory, { params: { operation_uuid: operateId, }, }) .then((res) => { handleResponse(res, (res) => { setStep3Data(res.data[0]); if (!timer.current) { res.data[0].detail_lst.map((item) => { setIsDetailOpen({ ...isDetailOpen, [item.service_name]: false, }); }); } if ( res.data[0]?.install_status == 1 || res.data[0]?.install_status == 0 ) { timer.current = setTimeout(() => { queryInstallationInfo(operateId); }, 2000); } }); }) .catch((e) => console.log(e)) .finally(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight; }); }; useEffect(() => { fetchData(); return () => { clearTimeout(timer.current); }; }, []); return (
{ dispatch(getTabKeyChangeAction("service")); history?.goBack(); // history?.push({ // pathname: `/application_management/app_store`, // }); }} /> {name}
{/* 第一步 */} {stepNum == 0 && ( <>
基本信息
{/* 服务信息 */}
服务信息
{currentproDependenceData && currentproDependenceData.pro_services && currentproDependenceData.pro_services.length == 0 ? (
) : ( currentproDependenceData?.pro_services?.map((item) => { return (
{item.name}:
); }) )}
依赖信息
{currentproDependenceData && currentproDependenceData.dependence_services_info && currentproDependenceData.dependence_services_info.length == 0 ? (
) : ( currentproDependenceData?.dependence_services_info?.map( (item) => ( ) ) )}
)} {/* 第二步 */} {stepNum == 1 && ( <> {/* 不遍历了,直接用第一项 */}
{currentproDependenceData?.pro_services[0]?.name}
{ setIsOpen({ ...isOpen, [currentproDependenceData?.pro_services[0]?.name]: !isOpen[ currentproDependenceData?.pro_services[0]?.name ], }); // setIsDetailOpen({ // ...isDetailOpen, // [item.ip]: !isDetailOpen[item.ip], // }); }} > 查看更多配置
{currentproDependenceData?.pro_services[0]?.app_install_args?.map( (item) => { return ( {item.name}} name={`install|${ currentproDependenceData?.pro_services[0]?.name }|${JSON.stringify({ name: item.name, key: item.key, dir_key: item.dir_key, })}`} rules={[ { required: true, message: `请填写${item.name}`, }, ]} > ) : null } /> ); } )} {currentproDependenceData?.pro_services[0]?.app_port?.map( (item) => { return ( {item.name}} name={`port|${ currentproDependenceData?.pro_services[0]?.name }|${JSON.stringify({ name: item.name, key: item.key, //dir_key: item.dir_key, })}`} rules={[ { required: true, message: `请填写${item.name}`, }, ]} > ) : null } /> ); } )}
{/* 展示jdk */} {showJdk ? ( <>
{ currentproDependenceData?.dependence_services_info[0] ?.name }
{/* {currentproDependenceData?.dependence_services_info[0] ?.is_pack_exist ? ( "" ) : (

{currentproDependenceData?.dependence_services_info[0]?.name} 服务的安装包不存在 !

)} */}
{ setIsOpen({ ...isOpen, [currentproDependenceData?.dependence_services_info[0] ?.name]: !isOpen[ currentproDependenceData ?.dependence_services_info[0]?.name ], }); }} > 查看更多配置
{currentproDependenceData?.dependence_services_info[0]?.app_install_args?.map( (i) => { return ( {i.name} } name={`install|${ currentproDependenceData ?.dependence_services_info[0]?.name }|${JSON.stringify({ name: i.name, key: i.key, dir_key: i.dir_key, })}`} rules={[ { required: true, message: `请填写${i.name}`, }, ]} > ) : null } /> ); } )} {currentproDependenceData?.dependence_services_info[0]?.app_port?.map( (i) => { return ( {i.name} } name={`port|${ currentproDependenceData ?.dependence_services_info[0]?.name }|${JSON.stringify({ name: i.name, key: i.key, })}`} rules={[ { required: true, message: `请填写${i.name}`, }, ]} > ) : null } /> ); } )}
) : ( "" )}
分布主机数量: 1台
)} {stepNum == 2 && ( <> {step3Data?.detail_lst?.map((item) => { return (
); })}
{step3Data.install_status_msg}{" "} {(step3Data.install_status == 0 || step3Data.install_status == 1) && ( )}
)}
); }; export default ApplicationInstallation; ================================================ FILE: omp_web/src/pages/AppStore/config/BatchInstallationModal.js ================================================ import { Button, Modal, Select, Switch } from "antd"; import { useEffect, useRef, useState } from "react"; import { CopyOutlined } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { OmpTable } from "@/components"; import { useHistory } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { getStep1ChangeAction } from "./Installation/store/actionsCreators"; import { getUniqueKeyChangeAction } from "../store/actionsCreators"; const BatchInstallationModal = ({ bIModalVisibility, setBIModalVisibility, dataSource, installTitle, initLoading, }) => { const uniqueKey = useSelector((state) => state.appStore.uniqueKey); const reduxDispatch = useDispatch(); const [loading, setLoading] = useState(false); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); // console.log(checkedList) //应用服务选择的版本号 const versionInfo = useRef({}); const columns = [ { title: "名称", key: "name", dataIndex: "name", align: "center", ellipsis: true, width: 80, render: (text, record) => { return text; }, }, { title: "版本", key: "version", dataIndex: "version", align: "center", ellipsis: true, width: 120, render: (text, record) => { return ( ); }, }, ]; // 高可用是否开启 const [highAvailabilityCheck, setHighAvailabilityCheck] = useState(false); // 批量安装/服务安装选择确认请求 const createInstallInfo = (install_product) => { setLoading(true); fetchPost(apiRequest.appStore.createInstallInfo, { body: { high_availability: highAvailabilityCheck, install_product: install_product, unique_key: uniqueKey, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { reduxDispatch(getStep1ChangeAction(res.data.data)); } history.push("/application_management/app_store/installation"); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 组件安装 const createComponentInstallInfo = (install_product) => { setLoading(true); fetchPost(apiRequest.appStore.createComponentInstallInfo, { body: { high_availability: highAvailabilityCheck, install_component: install_product, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { reduxDispatch(getStep1ChangeAction(res.data.data)); reduxDispatch(getUniqueKeyChangeAction(res.data.unique_key)); } history.push("/application_management/app_store/installation"); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { // 选中全部 setCheckedList(dataSource.filter((item) => item.is_continue)); }, [dataSource]); // console.log(checkedList) return ( {installTitle == "服务" ? "服务安装-选择版本" : installTitle == "组件" ? "组件安装-选择版本" : "批量安装-选择应用服务"} } afterClose={() => { setCheckedList([]); setHighAvailabilityCheck(false); }} onCancel={() => { setBIModalVisibility(false); }} visible={bIModalVisibility} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
{ return record.name; }} checkedState={[checkedList, setCheckedList]} pagination={false} notSelectable={(record) => ({ // is_continue的不能选中 disabled: !record.is_continue, })} rowSelection={{ selectedRowKeys: checkedList?.map((item) => item?.name), }} />
高可用 { setHighAvailabilityCheck(e); }} />
已选择 {checkedList.length} 个
共 {dataSource.length} 个
); }; export default BatchInstallationModal; ================================================ FILE: omp_web/src/pages/AppStore/config/ComponentInstallation.js ================================================ import { useEffect, useState, useRef } from "react"; import { useHistory } from "react-router-dom"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { Steps, Form, Input, Button, Select, Tooltip, message } from "antd"; import { LeftOutlined, DownOutlined, InfoCircleOutlined, LoadingOutlined, } from "@ant-design/icons"; import styles from "./index.module.less"; import RenderComDependence from "./component/RenderComDependence"; const step2Open = (num) => ({ marginTop: 10, minHeight: 30, height: num * 75, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }); const step2NotOpen = () => ({ height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }); const step3Open = () => ({ marginTop: 10, padding: 10, minHeight: 30, height: 240, transition: "all .2s ease-in", overflow: "hidden", color: "#fff", backgroundColor: "#222222", wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "pre-line", overflowY: "auto", overflowX: "hidden", }); const step3NotOpen = () => ({ height: 0, minHeight: 0, padding: 0, transition: "all .2s ease-in", overflow: "hidden", color: "#fff", backgroundColor: "#222222", wordWrap: "break-word", wordBreak: "break-all", whiteSpace: "pre-line", overflowY: "auto", overflowX: "hidden", }); const ComponentInstallation = () => { const [form] = Form.useForm(); const history = useHistory(); let pathArr = history.location.pathname.split("/"); let name = pathArr[pathArr.length - 1]; const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [stepNum, setStepNum] = useState(0); // setp2的查看更多配置是否是展开状态 const [isOpen, setIsOpen] = useState({ [name]: false, }); // step3的安装详情是否是展开状态 因为多个所以为对象 const [isDetailOpen, setIsDetailOpen] = useState({}); const [versionCurrent, setVersionCurrent] = useState(""); const [step1Data, setStep1Data] = useState({}); const [step2Data, setStep2Data] = useState({}); const [step3Data, setStep3Data] = useState({}); // 第二步校验通过后,存储数据 const [vPassedresData, setVPassedresData] = useState({}); const [processContinue, setProcessContinue] = useState(true); //定义校验弹出msessage函数 const verificationError = (item) => { if (item.process_continue == false) { setProcessContinue(false); message.warning(item.process_message); return false; } else { setProcessContinue(true); return true; } }; const containerRef = useRef(null); const timer = useRef(null); function fetchData() { setLoading(true); fetchGet(apiRequest.appStore.componentEntrance, { params: { app_name: name, }, }) .then((res) => { handleResponse(res, (res) => { verificationError(res.data[0]); //console.log(res.data); setDataSource(res.data); // 设置版本默认选中第一个 //console.log(form); setVersionCurrent(res.data[0].app_version); form.setFieldsValue({ version: res.data[0].app_version }); form.setFieldsValue({ clusterMode: JSON.stringify(res.data[0].deploy_mode[0]), }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } let currentAppDependenceData = dataSource.filter( (item) => item.app_version == versionCurrent )[0]; const [ipListSource, setIpListSource] = useState([]); const [searchLoading, setSearchLoading] = useState(false); const use_exist_servicesRef = useRef([]); const install_servicesRef = useRef([]); // 定义选中的ip是否包含在instance_info const hasSameIp = (item, ip) => { return ( item.is_base_env && item.instance_info && item.instance_info.filter((b) => b.ip == ip) ); }; const [showJdk, setShowJdk] = useState(false); const jdkHandleInitData = (data = [], ip) => { //console.log("jdk 尝试设置默认值"); data.map((item) => { if (hasSameIp(item, ip).length == 0) { //console.log("不存在相同"); setShowJdk(true); install_servicesRef.current = [ { ...item, }, ]; use_exist_servicesRef.current = []; item?.app_install_args?.map((i) => { setIsOpen({ ...isOpen, [i.name]: false, }); form.setFieldsValue({ [`install|${item.name}|${JSON.stringify({ name: i.name, key: i.key, dir_key: i.dir_key, })}`]: i.default, }); }); item?.app_port?.map((i) => { setIsOpen({ ...isOpen, [i.name]: false, }); form.setFieldsValue({ [`port|${item.name}|${JSON.stringify({ name: i.name, key: i.key, dir_key: i.dir_key, })}`]: i.default, }); }); form.setFieldsValue({ [`${item.name}|ip`]: ip, [`${item.name}|instanceName`]: `${item.name}-${ip[ip.length - 2]}-${ ip[ip.length - 1] }`, }); } else { setShowJdk(false); //console.log("存在相同"); let isSame = hasSameIp(item, ip)[0]; use_exist_servicesRef.current = [isSame]; install_servicesRef.current = []; } }); }; const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); const firstIP = res.data[0].split("."); form.setFieldsValue({ ip: res.data[0], instanceName: `${name}-${firstIP[firstIP.length - 2]}-${ firstIP[firstIP.length - 1] }`, }); let firstIp = res.data[0]; // jdk 数据默认设置 jdkHandleInitData(currentAppDependenceData?.app_dependence, firstIp); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; // 开始安装get const queryInstallationInfo = (operateId) => { fetchGet(apiRequest.appStore.installHistory, { params: { operation_uuid: operateId, }, }) .then((res) => { handleResponse(res, (res) => { setStep3Data(res.data[0]); if (!timer.current) { res.data[0].detail_lst.map((item) => { setIsDetailOpen({ ...isDetailOpen, [item.service_name]: false, }); }); } if ( res.data[0]?.install_status == 1 || res.data[0]?.install_status == 0 ) { timer.current = setTimeout(() => { queryInstallationInfo(operateId); }, 2000); } }); }) .catch((e) => console.log(e)) .finally(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight; }); }; useEffect(() => { fetchData(); return () => { clearTimeout(timer.current); }; }, []); return (
{ history?.goBack(); // history?.push({ // pathname: `/application_management/app_store`, // }); }} /> {name}
{/* 第一步 */} {stepNum == 0 && ( <>
基本信息
依赖信息
{currentAppDependenceData && currentAppDependenceData.app_dependence && currentAppDependenceData.app_dependence.length == 0 ? (
) : ( currentAppDependenceData?.app_dependence?.map((item) => ( )) )}
)} {/* 第二步 */} {stepNum == 1 && ( <>
{name}
{ setIsOpen({ ...isOpen, [name]: !isOpen[name], }); // setIsDetailOpen({ // ...isDetailOpen, // [item.ip]: !isDetailOpen[item.ip], // }); }} > 查看更多配置
{currentAppDependenceData?.app_install_args?.map((item) => { return ( {item.name}} name={`install|${ currentAppDependenceData?.app_name }|${JSON.stringify({ name: item.name, key: item.key, dir_key: item.dir_key, })}`} rules={[ { required: true, message: `请填写${item.name}`, }, ]} > ) : null } /> ); })} {currentAppDependenceData?.app_port?.map((item) => { return ( {item.name}} name={`port|${ currentAppDependenceData?.app_name }|${JSON.stringify({ name: item.name, key: item.key, //dir_key: item.dir_key, })}`} rules={[ { required: true, message: `请填写${item.name}`, }, ]} > ) : null } /> ); })}
{/* 渲染jdk */} {showJdk ? ( <> {currentAppDependenceData?.app_dependence?.map((item) => { return (
{item.name}
{/* {item.is_pack_exist ? ( "" ) : (

{item.name} 服务的安装包不存在 !

)} */}
{ setIsOpen({ ...isOpen, [item.name]: !isOpen[item.name], }); }} > 查看更多配置
{item?.app_install_args?.map((i) => { return ( {i.name} } name={`install|${item.name}|${JSON.stringify({ name: i.name, key: i.key, dir_key: i.dir_key, })}`} rules={[ { required: true, message: `请填写${i.name}`, }, ]} > ) : null } /> ); })} {item?.app_port?.map((i) => { return ( {i.name} } name={`port|${item.name}|${JSON.stringify({ name: i.name, key: i.key, })}`} rules={[ { required: true, message: `请填写${i.name}`, }, ]} > ) : null } /> ); })}
); })} ) : ( "" )}
分布主机数量: 1台
)} {stepNum == 2 && ( <> {step3Data?.detail_lst?.map((item) => { return (
); })}
{step3Data.install_status_msg}{" "} {(step3Data.install_status == 0 || step3Data.install_status == 1) && ( )}
)}
); }; export default ComponentInstallation; ================================================ FILE: omp_web/src/pages/AppStore/config/DeleteServerModal.js ================================================ import { Modal, Cascader, message } from "antd"; import { useEffect, useState } from "react"; //import BMF from "browser-md5-file"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; const DeleteServerModal = ({ deleteServerVisibility, setDeleteServerVisibility, tabKey, refresh, }) => { const [loading, setLoading] = useState(false); const [options, setOptions] = useState([]); const [resApp, setResApp] = useState([]); const [initData, setInitData] = useState([]); // 获取可删除选项 const queryData = () => { setLoading(true); fetchGet(apiRequest.appStore.deleteServer, { params: { type: tabKey === "component" ? "component" : "product", }, }) .then((res) => { handleResponse(res, (res) => { setInitData(res.data.data); setOptions( res.data.data.map((e) => { return { label: ( <> {e.name.includes("|") ? ( <> {e.name.split("|")[0]} {e.name.split("|")[1]} ) : ( e.name )} ), value: e.name, children: e.versions.map((i) => { return { label: ( <> {i.includes("|") ? ( <> {i.split("|")[0]} {i.split("|")[1]} ) : ( i )} ), value: i, }; }), }; }) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 删除操作 const doDelete = () => { if (resApp.length === 0) { message.info("请先选择要删除的服务"); return; } const allList = []; const someList = {}; const resData = []; for (let i = 0; i < resApp.length; i++) { const e = resApp[i]; if (e.length === 1) { allList.push(e[0]); } else { if (someList.hasOwnProperty(e[0])) { someList[e[0]].push(e[1]); } else { someList[e[0]] = [e[1]]; } } } for (let i = 0; i < initData.length; i++) { const e = initData[i]; if (allList.includes(e.name)) { resData.push(e); } else if (someList.hasOwnProperty(e.name)) { resData.push({ name: e.name, versions: someList[e.name], }); } } setLoading(true); fetchPost(apiRequest.appStore.deleteServer, { body: { type: tabKey === "component" ? "component" : "product", data: resData, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code === 0) { message.success("删除成功"); setDeleteServerVisibility(false); queryData(); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { queryData(); }, [tabKey]); return ( 删除{tabKey === "component" ? "基础组件" : "自研服务"} } afterClose={() => { setResApp([]); refresh(); }} onCancel={() => { setDeleteServerVisibility(false); }} visible={deleteServerVisibility} width={480} confirmLoading={loading} okText={loading ? "稍候" : "删除"} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose onOk={() => doDelete()} >
选择服务: setResApp(value)} multiple maxTagCount="responsive" placeholder={tabKey === "component" ? "选择基础组件" : "选择自研服务"} />
); }; export default DeleteServerModal; ================================================ FILE: omp_web/src/pages/AppStore/config/GetService.js ================================================ import { useHistory, useLocation } from "react-router-dom"; import styles from "./index.module.less"; import { useSelector } from "react-redux"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { Radio, Steps, Table, Button, Form, Checkbox, Row, Input, Tooltip, Collapse, Tag, Modal, Spin, Result, message, Select, Switch, } from "antd"; import { useState, useEffect } from "react"; import { LeftOutlined, CheckCircleFilled, CloseCircleFilled, CaretRightOutlined, InfoCircleOutlined, SyncOutlined, ExclamationCircleFilled, ZoomInOutlined, } from "@ant-design/icons"; const { Panel } = Collapse; const ServiceItem = ({ form, itemData, errInfo, productName }) => { const version = typeof itemData?.version === "string" ? itemData.version : itemData.version[0]; useEffect(() => { form.setFieldsValue({ [`${itemData.name}-base_dir`]: itemData.base_dir?.replace( "{data_path}", "" ), [`${itemData.name}-data_dir`]: itemData.data_dir?.replace( "{data_path}", "" ), [`${itemData.name}-log_dir`]: itemData.log_dir?.replace( "{data_path}", "" ), [`${itemData.name}-run_user`]: itemData.run_user, [`${itemData.name}-service_port`]: itemData.service_port, }); }, []); return (
( )} > {productName ? ( {`[ ${productName} ]`} ) : ( "" )} {itemData.name} {errInfo && ( {errInfo} )} } extra={version} key="1" className={"panelItem"} style={{ backgroundColor: "#f6f6f6" }} > / 数据分区} placeholder="请输入安装目录" suffix={ } /> / 数据分区} placeholder="请输入数据目录" suffix={ } /> / 数据分区} placeholder="请输入数据目录" suffix={ } />
); }; // 第一步 const Step1 = ({ setStepNum, getServiceData, setCheckServiceData, setServiceConnectionData, }) => { const ipArr = Object.keys(getServiceData?.ips || []); const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 选中ip数据 const [getServiceIpArr, setGetServiceIpArr] = useState([]); // 选中所有ip const [selectAllIp, setSelectAllIp] = useState(false); // 服务数据表单 const [serviceForm] = Form.useForm(); // 扫描中对话框 const [ingVisible, setIngVisible] = useState(false); // 加载 const [loading, setLoading] = useState(false); const getPathValue = (name, dirType) => { const value = serviceForm.getFieldValue(name + "-" + dirType); return value !== "" ? "{data_path}" + value : ""; }; // 服务纳管 const startGetService = () => { setLoading(true); setIngVisible(true); const serviceData = [...getServiceData.service]; const resArr = []; // 处理服务信息 for (let i = 0; i < serviceData.length; i++) { const element = serviceData[i]; if (element.hasOwnProperty("child")) { const childInfo = {}; childInfo[element.version[0]] = element.child[element.version[0]].map( (i) => { return { name: i.name, version: i.version, base_dir: getPathValue(i.name, "base_dir"), data_dir: getPathValue(i.name, "data_dir"), log_dir: getPathValue(i.name, "log_dir"), run_user: serviceForm.getFieldValue(`${i.name}-run_user`), service_port: serviceForm.getFieldValue(`${i.name}-service_port`), }; } ); resArr.push({ name: element.name, version: element.version, child: childInfo, }); } else { resArr.push({ name: element.name, version: element.version, base_dir: getPathValue(element.name, "base_dir"), data_dir: getPathValue(element.name, "data_dir"), log_dir: getPathValue(element.name, "log_dir"), run_user: serviceForm.getFieldValue(`${element.name}-run_user`), service_port: serviceForm.getFieldValue( `${element.name}-service_port` ), }); } } // 处理ip信息 const ipsRes = {}; for (const key in getServiceData.ips) { if (Object.hasOwnProperty.call(getServiceData.ips, key)) { const element = getServiceData.ips[key]; if (getServiceIpArr.indexOf(key) !== -1) { ipsRes[key] = element; } } } fetchPost(apiRequest.appStore.appConfCheck, { body: { data: { service: resArr, ips: ipsRes, is_continue: getServiceData.is_continue, }, }, }) .then((res) => { handleResponse(res, (res) => { setCheckServiceData(res.data); let connectData = {}; const targetData = res.data.ser_info; for (let index = 0; index < targetData.length; index++) { const element = targetData[index]; connectData[element.name] = { is_use_exist: element.is_use_exist, exist_instance: element.exist_instance.length === 0 ? [] : [element.exist_instance[0]], }; } setServiceConnectionData(connectData); setStepNum(1); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setIngVisible(false); }); }; return (
请选择本次纳管的主机范围,填写服务相关信息

选择主机 : { const target = !selectAllIp; setSelectAllIp(target); setGetServiceIpArr(target ? ipArr : []); }} > 全选

{ipArr?.length === 0 ? ( 暂无可选的主机 ) : ( { setGetServiceIpArr(checkedValues); setSelectAllIp( checkedValues.length === ipArr.length ? true : false ); }} style={{ marginLeft: 30, }} > {ipArr.map((e) => { return ( {e} ); })} )}
{getServiceData?.service?.map((item) => { if (item.hasOwnProperty("child")) { const childArr = item.child[item.version[0]]; return childArr.map((i) => { return ( ); }); } return ( ); })}
分布主机: {getServiceIpArr.length} 台
服务纳管 } visible={ingVisible} width={600} bodyStyle={{ fontSize: 14, }} onCancel={() => { setIngVisible(false); }} footer={null} >
正在按照服务相关信息,对指定服务器进行扫描,请稍后...
); }; // 第二步 const Step2 = ({ setStepNum, getServiceData, checkServiceData, serviceConnectionData, setServiceConnectionData, }) => { const tableData = checkServiceData?.ser_info; // 加载 const [loading, setLoading] = useState(false); const addService = () => { const resSerInfo = []; const sourceData = checkServiceData.ser_info; for (let index = 0; index < sourceData.length; index++) { const element = sourceData[index]; resSerInfo.push({ name: element.name, error: element.error, ip: element.ip, is_use_exist: serviceConnectionData[element.name].is_use_exist, exist_instance: serviceConnectionData[element.name].exist_instance, }); } setLoading(true); fetchPost(apiRequest.appStore.appConfCheck, { body: { data: { ser_info: resSerInfo, uuid: checkServiceData.uuid, }, }, }) .then((res) => { handleResponse(res, (res) => { if (res.data.is_error) { message.warning(res.data.message); } else { setStepNum(2); } }); }) .catch((e) => console.log(e)) .finally(() => setLoading(false)); }; const tableColumn = [ { title: "服务名称", dataIndex: "name", key: "name", align: "center", width: 200, }, { title: "扫描到该服务的IP", dataIndex: "ip", key: "ip", align: "center", width: 300, filters: Object.keys(getServiceData?.ips || []).map((i) => { return { value: i, text: i, }; }), onFilter: (value, record) => record.ip.indexOf(value) !== -1, render: (text) => { if (typeof text === "string") { return text; } else if (text.length === 0) { return "-"; } else { return ( <> {text.map((i) => ( {i} ))} ); } }, }, { title: "关联集群", dataIndex: "is_use_exist", key: "is_use_exist", align: "center", width: 80, render: (text, record) => { return ( { const resData = {}; for (const key in serviceConnectionData) { if (Object.hasOwnProperty.call(serviceConnectionData, key)) { const element = serviceConnectionData[key]; if (key === record.name) { resData[key] = { is_use_exist: value, exist_instance: element.exist_instance, }; } else { resData[key] = element; } } } setServiceConnectionData(resData); }} /> ); }, }, { title: "可选集群实例", dataIndex: "exist_instance", key: "exist_instance", align: "center", width: 260, render: (text, record) => { if (record.exist_instance.length === 0) { return "无可用实例"; } else { return (
); }; const GetService = () => { const history = useHistory(); const location = useLocation(); // 服务纳管初始数据 const getServiceData = location.state?.getServiceData; // 服务扫描信息 const [checkServiceData, setCheckServiceData] = useState(null); // 服务关联信息 const [serviceConnectionData, setServiceConnectionData] = useState({}); // 步骤 const [stepNum, setStepNum] = useState(0); return (
{ history?.goBack(); }} /> 服务纳管
{stepNum == 0 && ( )} {stepNum == 1 && ( )} {stepNum == 2 && ( { history?.goBack(); }} > 返回 , , ]} /> )}
); }; export default GetService; ================================================ FILE: omp_web/src/pages/AppStore/config/GetServiceModal.js ================================================ import { Button, Modal, Input, Select } from "antd"; import { useEffect, useRef, useState } from "react"; import { CopyOutlined, SearchOutlined } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { OmpTable } from "@/components"; import { useHistory } from "react-router-dom"; const GetServiceModal = ({ modalVisibility, setModalVisibility, initData, dataSource, setDataSource, initLoading, }) => { const [loading, setLoading] = useState(false); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); //应用服务选择的版本号 const versionInfo = useRef({}); const [searchName, setSearchName] = useState(""); const columns = [ { title: "名称", key: "name", dataIndex: "name", align: "center", ellipsis: true, width: 80, render: (text, record) => { return text; }, }, { title: "版本", key: "version", dataIndex: "version", align: "center", ellipsis: true, width: 120, render: (text, record) => { if (typeof text === "string") { return text; } else if (text.length === 1) { return text[0]; } else { return ( ); } }, }, ]; const checkServiceData = () => { // setLoading(true); const copyCheckData = [...checkedList]; // 处理选中数据 const childArr = copyCheckData.filter((i) => typeof i.version === "string"); const resData = copyCheckData .filter((i) => typeof i.version !== "string") .map((i) => { const version = versionInfo.current[i.name] || i.version[0]; const itemData = { name: i.name, version: [version], }; if (i.hasOwnProperty("child")) { const childOjb = {}; childOjb[version] = i.child[version]?.filter( (i) => childArr.indexOf(i) !== -1 ); itemData.child = childOjb; } return itemData; }); fetchPost(apiRequest.appStore.queryAppList, { body: { data: resData, }, }) .then((res) => { handleResponse(res, (res) => { setLoading(false); if (res.data) { history.push({ pathname: "/application_management/get-service", state: { getServiceData: res.data, }, }); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => {}, []); return ( 选择纳管服务 } afterClose={() => { setCheckedList([]); versionInfo.current = {}; }} onCancel={() => { setModalVisibility(false); }} visible={modalVisibility} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, marginTop: -10, }} destroyOnClose >
} style={{ width: 220, }} value={searchName} onChange={(e) => { setSearchName(e.target.value); if (e.target.value === "") { setDataSource(initData); } }} onPressEnter={() => { setDataSource( initData.filter((i) => i.name.includes(searchName)) ); }} />
已选择{" "} {checkedList.filter((i) => typeof i.version !== "string").length}{" "} 个
{ return record.name; }} checkedState={[checkedList, setCheckedList]} pagination={false} rowSelection={{ selectedRowKeys: checkedList?.map((item) => item?.name), onSelect: (record, selected, selectedRows, nativeEvent) => { if (selected) { // 如果有子项则全部选中 let childArr = []; if (record.hasOwnProperty("child")) { const version = versionInfo.current[record.name] || record.version[0]; childArr = record.child[version]; } // 如果有父则选中 let fatherArr = []; if (typeof record.version === "string") { for (let i = 0; i < dataSource.length; i++) { const element = dataSource[i]; if (element.hasOwnProperty("child")) { const version = versionInfo.current[element.name] || element.version[0]; if (element.child[version].indexOf(record) !== -1) { fatherArr.push(element); break; } } } } setCheckedList( Array.from( new Set([ ...checkedList, ...childArr, ...fatherArr, record, ]) ) ); } else { let arr = [record.name]; if (record.hasOwnProperty("child")) { const version = versionInfo.current[record.name] || record.version[0]; arr = [...arr, ...record.child[version].map((i) => i.name)]; } setCheckedList( checkedList.filter((i) => arr.indexOf(i.name) === -1) ); } }, onSelectAll: (selected, selectedRows, changeRows) => { if (selected) { let resArr = []; if ( !(changeRows.length === 1 && changeRows[0] === undefined) ) { for (let i = 0; i < dataSource.length; i++) { const element = dataSource[i]; resArr.push(element); const version = versionInfo.current[element.name] || element.version[0]; if (element.hasOwnProperty("child")) { resArr = [...resArr, ...element.child[version]]; } } } setCheckedList(Array.from(new Set(resArr))); } else { setCheckedList([]); } }, }} />
); }; export default GetServiceModal; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/BasicInfoItem/index.js ================================================ import styles from "../index.module.less"; import { Form, Input, InputNumber } from "antd"; import { useEffect, useState, useRef } from "react"; import { randomNumber } from "@/utils/utils"; import { DownOutlined } from "@ant-design/icons"; const BasicInfoItem = ({ data, form }) => { // step3的安装详情是否是展开状态 因为多个所以为对象 const [isOpen, setIsOpen] = useState(false); const numRef = useRef({}); const step2Open = (num) => ({ marginTop: 10, minHeight: 30, height: num * 55, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", display: "flex", padding: 10, flexWrap: "wrap", }); const step2NotOpen = () => ({ height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", display: "flex", }); useEffect(() => { form.setFieldsValue({ [`${data.name}`]: `${data.name}-${randomNumber()}`, }); data.services_list.map((item) => { numRef.current[ `${data.name}=${item.name}` ] = `${item.deploy_mode.default}`; form.setFieldsValue({ [`${data.name}=${item.name}`]: `${item.deploy_mode.default}`, }); }); }, []); return ( <>
{data.name}
{data.version}
{data.services_list.map((item) => { return (
{item.name}} name={`${data.name}=${item.name}`} style={{ marginBottom: 0 }} > { if ( e && (e - item.deploy_mode.step == numRef.current[`${data.name}=${item.name}`] || e + item.deploy_mode.step == numRef.current[`${data.name}=${item.name}`]) ) { numRef.current[`${data.name}=${item.name}`] = e; } else { form.setFieldsValue({ [`${data.name}=${item.name}`]: numRef.current[`${data.name}=${item.name}`], }); } }} keyboard={false} disabled={item.deploy_mode.step == 0} step={item.deploy_mode.step} />
); })}
{data.error_msg}
); }; export default BasicInfoItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/DeployInstanceRow.js ================================================ import { Select, Form, Checkbox } from "antd"; import { useEffect, useState } from "react"; import RenderArr from "./RenderArr"; import RenderNum from "./RenderNum"; const DeployInstanceRow = ({ data, form }) => { const [check, setCheck] = useState(true); useEffect(() => { if (check) { form.setFieldsValue({ [`${data.name}`]: JSON.stringify({ name: data.exist_instance[0]?.name, id: data.exist_instance[0]?.id, type: data.exist_instance[0]?.type, }), }); } }, [check]); return ( <>
{data.name}
{data.version}
{check ? ( <>
) : Array.isArray(data.deploy_mode) ? ( ) : ( )}
{ setCheck(e.target.checked); }} > 复用依赖
); }; export default DeployInstanceRow; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/DeployNumRow.js ================================================ import RenderArr from "./RenderArr"; import RenderNum from "./RenderNum"; const DeployNumRow = ({ data, form }) => { return ( <>
{data.name}
{data.version}
{Array.isArray(data.deploy_mode) ? ( ) : ( )}
); }; export default DeployNumRow; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/DeployRow.js ================================================ import DeployNumRow from "./DeployNumRow"; import DeployInstanceRow from "./DeployInstanceRow"; const DeployRow = ({ data, form }) => { return <>{data.is_use_exist ? : }; }; export default DeployRow; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/JdkRow.js ================================================ import { Checkbox } from "antd"; const JdkRow = ({ data, isBaseEnv }) => { return ( <>
{data.name}
{data.version}
{!isBaseEnv && ( 安装依赖 )}
); }; export default JdkRow; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/RenderArr.js ================================================ import { Select, Form, Input } from "antd"; import { useEffect, useState } from "react"; import { randomNumber } from "@/utils/utils"; const RenderArr = ({ data, form }) => { const [deployValue, setDeployValue] = useState(data.deploy_mode[0]?.key); useEffect(() => { form.setFieldsValue({ [`${data.name}=num`]: deployValue, }); if (deployValue == "master-slave" || deployValue == "master-master") { form.setFieldsValue({ [`${data.name}=name`]: `${data.name}-cluster-${randomNumber(7)}`, }); } }, [deployValue]); return ( <>
{(deployValue == "master-slave" || deployValue == "master-master") && ( )}
{deployValue == "master-master" && ( )}
); }; export default RenderArr; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/component/RenderNum.js ================================================ import { Form, InputNumber, Input } from "antd"; import { useEffect, useRef, useState } from "react"; import { randomNumber } from "@/utils/utils"; const RenderNum = ({ data, form }) => { const [num, setNum] = useState(data.deploy_mode.default); const numRef = useRef(data.deploy_mode.default); useEffect(() => { if (num == 1) { form.setFieldsValue({ [`${data.name}=num`]: `${data.deploy_mode.default}`, }); } if (num > 1) { form.setFieldsValue({ [`${data.name}=num`]: `${data.deploy_mode.default}`, }); form.setFieldsValue({ [`${data.name}=name`]: `${data.name}-cluster-${randomNumber(7)}`, }); } }, []); return ( <>
{ if (e) { if ( e - data.deploy_mode.step == numRef.current || e + data.deploy_mode.step == numRef.current ) { numRef.current = e; setNum(e); } else { form.setFieldsValue({ [`${data.name}=num`]: numRef.current, }); } } if (e > 1) { form.setFieldsValue({ [`${data.name}=name`]: `${data.name}-cluster-${randomNumber( 7 )}`, }); } }} keyboard={false} disabled={data.deploy_mode.step == 0} step={data.deploy_mode.step} min={1} max={32} style={{ width: 100, }} />
{num > 1 && ( )}
); }; export default RenderNum; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/DependentinfoItem/index.js ================================================ import styles from "../index.module.less"; import JdkRow from "./component/JdkRow"; import DeployRow from "./component/DeployRow"; const DependentInfoItem = ({ data, form, isBaseEnv }) => { // useEffect(() => { // form.setFieldsValue({ // [`${data.name}`]: `${data.name}-${randomNumber()}`, // }); // data.services_list.map((item) => { // form.setFieldsValue({ // [`${data.name}=${item.name}`]: `${item.deploy_mode.default}`, // }); // }); // }, []); return ( <>
{data.is_base_env ? ( ) : ( )}
{data.error_msg}
); }; export default DependentInfoItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/InstallInfoItem/component/InstallDetail.js ================================================ import { DownOutlined } from "@ant-design/icons"; import { useEffect, useRef } from "react"; const stepOpen = { marginTop: 10, minHeight: 30, height: 300, transition: "all .2s ease-in", //overflow: "hidden", backgroundColor: "#000", color: "#fff", padding: 10, overflowY: "auto", whiteSpace: "pre-line" }; const stepNotOpen = { height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }; // 状态渲染规则 const renderStatus = { 0: 等待安装, 1: 正在安装, 2: 安装成功, 3: 安装失败, }; const InstallDetail = ({ title, ip, status, openName, setOpenName,log }) => { const containerRef = useRef(null) useEffect(()=>{ containerRef.current.scrollTop = containerRef.current.scrollHeight; },[log]) return ( ); }; export default InstallDetail; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/InstallInfoItem/index.js ================================================ import InstallDetail from "./component/InstallDetail"; const InstallInfoItem = ({ id, data, title, openName, setOpenName, log, idx, }) => { return (
{title}
{data.map((item) => { return ( ); })}
); }; export default InstallInfoItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/ServiceConfigItem/index.js ================================================ import { Collapse, Form, Input, Tooltip, Spin } from "antd"; import { CaretRightOutlined, InfoCircleOutlined } from "@ant-design/icons"; import { useEffect } from "react"; import { useDispatch, useSelector } from "react-redux"; const { Panel } = Collapse; const ServiceConfigItem = ({ form, loading, ip, idx }) => { let data = useSelector((state) => state.installation.step3Data)[ip][idx]; // console.log(data) let portData = data.ports || []; let installArgsData = data.install_args || []; const renderData = [...installArgsData, ...portData]; // let instanceName = data.instance_name; const errInfo = useSelector((state) => state.installation.step3ErrorData); useEffect(() => { // 设置默认值 // form.setFieldsValue({ // [`${data.name}=instance_name`]: data.instance_name, // }); renderData.map((i) => { form.setFieldsValue({ [`${data.name}=${i.key}`]: i.default, }); }); }, [data]); useEffect(() => { if (errInfo[ip] && errInfo[ip][data.name]) { for (const key in errInfo[ip][data.name]) { if (errInfo[ip][data.name][key]) { form.setFields([ { name: `${data.name}=${key}`, errors: [errInfo[ip][data.name][key]], }, ]); } } } return () => { form.resetFields(); }; }, [errInfo[ip]]); return (
( )} > {renderData.map((item) => { return ( { // //console.log(e.target.value); // dispatch( // getStep3ServiceChangeAction( // ip, // data.name, // item.key, // e.target.value // ) // ); // }} addonBefore={ item.dir_key ? ( / 数据分区 ) : null } //style={{ width: 420 }} placeholder={`请输入${item.name}`} suffix={ item.dir_key ? ( ) : null } /> ); })}
); }; export default ServiceConfigItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/ServiceDistributionItem/component/HasInstallService.js ================================================ import { Tooltip, Spin } from "antd"; const HasInstallService = ({ children, ip, installService }) => { return (
已选安装服务
{installService[ip]?.map((item) => { return (
{item.service_instance_name}
); })}
} > {children} ); }; export default HasInstallService; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/ServiceDistributionItem/index.js ================================================ import { Cascader, Form, Tag, Tooltip } from "antd"; import { useEffect, useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { getDataSourceChangeAction, getIpListChangeAction, } from "../../store/actionsCreators"; import * as R from "ramda"; import HasInstallService from "./component/HasInstallService"; // 当前组件一次value改变,伴随着三个动作 // 1. 组件state的value 的改变 // 2. redux中dataSource 的改变 // (1)ipList 的改变,影响页面中`已分配主机数量` // (2) dataSource的改变 (对应组件中的option,决定组件展开框的渲染) // 3. form中数据的变更 // 当前组件触发value改变的情况有 // 1. 点击展开栏的check或者item容器或者文字 // 2. 点击展示框的tag // 3. 展示框关于with的处理 // update 2021.12.30 // 为解决关于with项关联元素异常展示问题 // 在onclick 事件中添加两个动作 // 在执行onclick正常逻辑之前判断当前元素内是否全部子项都是不可选中状态,如果是直接return,不再执行正常逻辑(或者直接判断当前元素类名是否有disable相关) // 在执行onclick正常逻辑或者执行完关联with之后,查询其他元素内是否存在子元素全是不可选中状态的元素,将这种元素设置成不可选中状态 const ServiceDistributionItem = ({ form, data, info, installService }) => { const [options, setOption] = useState([]); const [value, setValue] = useState([]); const allDataPool = useSelector((state) => state.installation.dataSource); const ipList = useSelector((state) => state.installation.ipList); const errorList = useSelector((state) => state.installation.errorList); const errorMsg = errorList.filter((i) => i.ip == info.ip)[0]?.error_msg; const reduxDispatch = useDispatch(); // 对value进行处理(因为当把某一级菜单下对应的全部二级菜单选中后,value会合并成一个,只会存展示一级菜单) const dealColumnsData = (value) => { let result = []; value.map((item) => { //console.log(item); switch (item.length) { case 1: // value长度为1时,代表选中全部的一级菜单 let checkedItem = options .filter((i) => { return i.label == item[0]; })[0] .children.map((i) => { return i.label; }); result = result.concat([...checkedItem]); break; case 2: // console.log(item); result.push(item[1]); break; default: return []; break; } }); return result; }; const handleDataSourceData = (key, num) => { if (Array.isArray(key)) { key.forEach((item) => { if (allDataPool[item].num >= 0) { let n = allDataPool[item].num + num <= 0 ? 0 : allDataPool[item].num + num; allDataPool[item].num = n; } }); return allDataPool; } else { if (allDataPool[key].num >= 0) { let n = allDataPool[key].num + num <= 0 ? 0 : allDataPool[key].num + num; allDataPool[key].num = n; } return allDataPool; } }; // 当选中这项时同时拿到所有with这项的数据 const getWithItem = (label) => { // 全部with当前项的数据 const result = []; for (const key in allDataPool) { // console.log(allDataPool[key]); if (allDataPool[key].with && allDataPool[key].with == label) { result.push(key); } } return result; }; // 封装对选中被其他项with关联的数据的处理(选中) const handleWithData = (label, withItem, isCheck) => { if (withItem.length > 0) { withItem.map((w) => { let formData = R.clone(form.getFieldsValue()); // 确认with这项是属于哪个product let optionsCopy = R.clone(options); let productItem = optionsCopy.filter((item) => { let f = item.children.filter((i) => { return i.label == w; }); return f.length !== 0; }); if (isCheck) { formData[info.ip].push([productItem[0].label, w]); // form.setFieldsValue({ // [`${info.ip}`]: formData[info.ip], // }); setValue((value) => { let strArr = [ ...formData[info.ip], ...value, [productItem[0].label, w], ].map((item) => JSON.stringify(item)); let delStrArr = Array.from(new Set([...strArr])); return [...delStrArr.map((item) => JSON.parse(item))]; }); // 数据池子中数量变更 let data = handleDataSourceData(w, -1); reduxDispatch(getDataSourceChangeAction(data)); } else { let withI = [productItem[0]?.label, w]; let result = formData[info.ip].filter((item) => { return item[0] != withI[0] || item[1] != withI[1]; }); // form.setFieldsValue({ // [`${info.ip}`]: result, // }); let arr = []; result.map((v) => { Object.keys(allDataPool).map((i) => { if (v[1] == allDataPool[i].with) { arr.push([v[0], i]); } }); }); setValue([...result, ...arr]); let dataC = handleDataSourceData(w, 1); reduxDispatch(getDataSourceChangeAction(dataC)); } }); } else { handleValueAndForm(label, isCheck); } }; const handleValueAndForm = (label, isCheck) => { // value的格式[[1,1],[1,2]] // setVal1ue() // form的格式 // 当前ip下和value一致 // 判断一下当前label是一级还是二级 if (allDataPool[label]) { // 二级 if (isCheck === true) { let result = [...value]; options.map((item) => { item.children.map((i) => { if (i.value == label) { result.push([item.label, i.label]); } }); }); setValue(result); // form.setFieldsValue({ // [`${info.ip}`]: result, // }); } else if (isCheck === false) { // 取消二级选中 let result = [...value]; let deleteItem = []; options.map((item) => { item.children.map((i) => { if (i.value == label) { deleteItem = [item.label, i.label]; } }); }); result = result.filter((item) => { return item[0] != deleteItem[0] || item[1] != deleteItem[1]; }); setValue(result); // form.setFieldsValue({ // [`${info.ip}`]: result, // }); } } else { // 一级 if (isCheck === true) { // console.log("点击到了一级的选中"); // console.log(options, isCheck); // 拿到一级下的全部子项 let checkedArr = options .filter((i) => { return i.label == label; })[0] .children.map((i) => { return i.label; }); // 去除其中带有with的 checkedArr = checkedArr.filter((i) => { if (allDataPool[i] && allDataPool[i].with) { return false; } return true; }); let result = checkedArr.map((item) => { return [label, item]; }); // 查找是否子项中有被其他项with的 let withArr = []; options.map((item) => { item.children.map((i) => { // if(!allDataPool[i.label]){ // console.log(i.label) // } let withI = allDataPool[i.label].with; let idx = checkedArr.indexOf(withI); if (withI && idx !== -1) { result.push([item.label, i.label]); withArr.push(i.label); } }); }); let data = handleDataSourceData(withArr, -1); reduxDispatch(getDataSourceChangeAction(data)); // console.log(value, result, label) let dealValue = value.filter((i) => { if (i[0] !== label) { return true; } //console.log(i) return false; }); setValue((v) => { let arr = Array.from( new Set([...dealValue, ...result].map((i) => JSON.stringify(i))) ); return arr.map((m) => JSON.parse(m)); // Array.from(new Set([...dealValue, ...result])) }); // form.setFieldsValue({ // [`${info.ip}`]: Array.from(new Set([...dealValue,...result])), // }); } else if (isCheck === false) { // console.log("点击到了一级的取消选中"); let withArr = []; let v = value.filter((i) => { // 当这项包含with直接留在这里不动,with项的改变在与被with项 if (allDataPool[i[1]] && allDataPool[i[1]].with) { // 判断这项的with是否包涵在value let arr = value.filter((a) => { return a[1] == allDataPool[i[1]].with; }); withArr.push(i[1]); return arr.length == 0; } return i[0] !== label; }); let data = handleDataSourceData(withArr, 1); reduxDispatch(getDataSourceChangeAction(data)); setValue(v); // form.setFieldsValue({ // [`${info.ip}`]: v, // }); } } }; // 获得子项全部是disable的项的lable // 并且当前已选中项中不存在 const getDisableItem = (options) => { let disableItem = []; options.forEach((i) => { if (i.children.filter((a) => !a.disabled) == 0) { disableItem.push(i.value); } }); console.log(disableItem, value, "要禁用的项"); return disableItem; }; // 根据lable,将其dom设置为不可用状态 const setDisableItem = (disableItem) => { if (disableItem.length > 0) { let arr = [...document.getElementsByClassName("ant-cascader-menus")]; arr.forEach((a) => { let dom = a?.childNodes; if (dom && dom[0].childNodes) { [...dom[0].childNodes].map((v) => { if (v && v.childNodes[1]) { if (disableItem.indexOf(v.childNodes[1].innerHTML) !== -1) { // v.childNodes[0].removeEventListener("click") // console.log(v.childNodes[0].getAttribute()) v.childNodes[0].onclick = (e) => { e.stopPropagation(); }; v.childNodes[0].classList.add("ant-cascader-checkbox-disabled"); v.style.cssText = "color: rgba(0,0,0,0.25);font-weight:400 !important;cursor:not-allowed"; } } }); } }); } }; // remakeDom重置dom const remakeDom = () => { let arr = [...document.getElementsByClassName("ant-cascader-menus")]; arr.forEach((a) => { let dom = a?.childNodes; if (dom && dom[0].childNodes) { [...dom[0].childNodes].map((v) => { if (v && v.childNodes[1]) { // v.childNodes[0].removeEventListener("click") // console.log(v.childNodes[0].getAttribute()) v.childNodes[0].onclick = null; v.childNodes[0].classList.remove("ant-cascader-checkbox-disabled"); v.style.cssText = "color: rgba(0, 0, 0, 0.65);font-weight:400;"; } }); } }); }; useEffect(() => { let disabledItem = getDisableItem(options); let isDelete = Object.keys(allDataPool).filter((k) => { // 当前组件实例已经选中了,那即使在数据池中该数据num已经为0,也不应影响组件对该数据的展示,在这里过滤掉 // 并且没有with项 return ( allDataPool[k].num == 0 && !dealColumnsData(value).includes(k) && !allDataPool[k].with ); // && disabledItem.indexOf(k) !== -1; }); let c = [...data]; c = c.map((i) => { // 去除在child对应数据池num为0的项 let child = [...i.child]; // if(disabledItem.indexOf(i.name) == -1) { child = i.child.filter((e) => { // let withI = data // .filter((f) => disabledItem.indexOf(f.name) !== -1) // .map((c) => { // console.log(c) // return c.child // }) // .flat(); return isDelete.indexOf(e) == -1; // || withI.indexOf(e) !== -1; }); // } return { ...i, child: child, }; }); setOption(() => { let result = []; c.map((item) => { let i = { label: item.name, value: item.name, }; // 当前项如果存在with就不可选中 i.children = item.child.map((n) => { let disabled = false; if (allDataPool[n] && allDataPool[n].with) { disabled = true; } return { label: n, value: n, disabled: disabled, }; }); if ( item.child.length > 0 && dealColumnsData(value) // || disabledItem.indexOf(i.label) !== -1 ) { // 当选中后,全部数据中有子项都是disable的 result.push(i); } }); return result; }); }, [allDataPool]); // 当value值发生改变时触发事件,用来判断当前组件所对应主机是否已经选择服务 useEffect(() => { form.setFieldsValue({ [`${info.ip}`]: value, }); let idx = ipList.indexOf(info.ip); // console.log(ipList, value, idx); if (value.length == 0) { if (idx !== -1) { //console.log([...ipList], info.ip); let newIpList = [...ipList]; newIpList.splice(idx, 1); reduxDispatch(getIpListChangeAction(newIpList)); } } else { if (idx == -1) { let newIpList = [...ipList]; newIpList.push(info.ip); reduxDispatch(getIpListChangeAction(newIpList)); } } }, [value]); return (
{errorMsg}
{ const { value, onClose, label } = e; return ( {allDataPool[label] ? label : `${label}-集合`} ); }} // tagRender={(e) => { // // console.log(e.onClose); // // console.log(e); // const { value, onClose, label } = e; // console.log(value); // if (value.includes("__RC_CASCADER_SPLIT__")) { // return ( // { // // onClose(event); // // }} // > // {label} // // ); // } else { // // 选中了一级菜单 // console.log(value); // let checkedItem = options // .filter((i) => { // return i.label == value; // })[0] // .children.map((i) => { // return i.label; // }); // console.log(checkedItem); // return ( // <> // {checkedItem.map((item) => { // return ( // { // // onClose(event); // // }} // > // {item} // // ); // })} // // ); // } // }} onClick={(e) => { // 会出现options的子项已经为空,但是一级还在的情况,在这里过滤 // setOption((op)=>{ // return op.filter(i=>{ // return i.children.length // }) // }) // 使用onclick的原因是因为onchange在每次点击后会触发两次, // 每次onchange执行都是一次单独逻辑,不能在每次onchange时,故不能准确对应整个数据池的num增减情况 // 点击总共会出现三种情况 // 1. 点击了checkbox // 2. 点击了背后的容器 // 3. 点击了文字 // console.log(e.target); // 因为antd是复用展开框,所以在设置禁用前先重制,然后再根据情况去确定是否设置禁用 remakeDom(); // 设置禁用项 let disabledItem = getDisableItem(options); setDisableItem(disabledItem); // 1. 点击了checkbox(这个比较特殊,也可能是点击了一级菜单触发) if (e.target.className == "ant-cascader-checkbox-inner") { // return // 选中(可能是一级也可能是二级,点击了checkbox) // 判断是点击的一级还是二级 let label = e.target.parentNode.parentNode.childNodes[1].innerHTML; if (allDataPool[label]) { // 点的是二级 if (allDataPool[label] && allDataPool[label].with) { return; } // with的判断不光是选中这项,还有判断当前选中这项有没有被其他的项with let withItem = getWithItem(label); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData(label, withItem, true); handleValueAndForm(label); let data = handleDataSourceData(label, -1); // console.log(data); reduxDispatch(getDataSourceChangeAction(data)); } else { // 点的是一级 // 在这个条件中还有一个情况是半选中状态 //console.log("点击一级"); if (disabledItem.indexOf(label) !== -1) { setDisableItem(disabledItem); return; } else { // 在点击前 let checkedArr = options .filter((i) => { return i.label == label; })[0] .children.map((i) => { return i.label; }); // 对checkedArr再做一次过滤,过滤掉含有with的项 checkedArr = checkedArr.filter((i) => { if (allDataPool[i] && allDataPool[i].with) { return false; } return true; }); // 还要过滤掉已经选中状态的 // value适配数据只要第二级数据 const hasCheck = value.map((item) => { return item[1]; }); checkedArr = checkedArr.filter((i) => { if (hasCheck.includes(i)) { return false; } return true; }); handleValueAndForm(label, true); let data = handleDataSourceData(checkedArr, -1); reduxDispatch(getDataSourceChangeAction(data)); } } // console.log( // "选中", // e.target.parentNode.parentNode.childNodes[1].innerHTML // ); } else if ( e.target.className == "ant-cascader-checkbox ant-cascader-checkbox-checked" ) { // reduxDispatch(getDataSourceChangeAction(data)); // 取消选中(可能是一级也可能是二级,点击了checkbox // 判断是点击的一级还是二级 let label = e.target.parentNode.childNodes[1].innerHTML; if (allDataPool[label]) { if (allDataPool[label] && allDataPool[label].with) { return; } // 点的是二级 // console.log("取消选中了一级") let withItem = getWithItem(label); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData(label, withItem, false); let data = handleDataSourceData(label, 1); reduxDispatch(getDataSourceChangeAction(data)); } else { // 点的是一级 let checkedArr = options .filter((i) => { return i.label == label; })[0] .children.map((i) => { return i.label; }); // 对checkedArr再做一次过滤,过滤掉含有with的项 checkedArr = checkedArr.filter((i) => { if (allDataPool[i] && allDataPool[i].with) { return false; } return true; }); handleValueAndForm(label, false); let data = handleDataSourceData(checkedArr, 1); reduxDispatch(getDataSourceChangeAction(data)); } // console.log( // "取消选中", // e.target.parentNode.childNodes[1].innerHTML // ); } // 2. 点击了背后的容器 if ( e.target.className == "ant-cascader-menu-item" || e.target.className == "ant-cascader-menu-item ant-cascader-menu-item-active" ) { if (e.target.getAttribute("aria-checked") === "true") { if ( allDataPool[e.target.lastChild.innerHTML] && allDataPool[e.target.lastChild.innerHTML].with ) { return; } // with的判断不光是选中这项,还有判断当前选中这项有没有被其他的项with let withItem = getWithItem(e.target.lastChild.innerHTML); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData( e.target.lastChild.innerHTML, withItem, false ); let data = handleDataSourceData( e.target.lastChild.innerHTML, 1 ); reduxDispatch(getDataSourceChangeAction(data)); // console.log( // "点击了背后容器,取消", // e.target.lastChild.innerHTML // ); } else { if ( allDataPool[e.target.lastChild.innerHTML] && allDataPool[e.target.lastChild.innerHTML].with ) { return; } // with的判断不光是选中这项,还有判断当前选中这项有没有被其他的项with let withItem = getWithItem(e.target.lastChild.innerHTML); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData( e.target.lastChild.innerHTML, withItem, true ); let data = handleDataSourceData( e.target.lastChild.innerHTML, -1 ); reduxDispatch(getDataSourceChangeAction(data)); // console.log( // "点击了背后容器,选中", // e.target.lastChild.innerHTML // ); } } // 3. 点击了文字 if (e.target.className == "ant-cascader-menu-item-content") { // 这个文字要判断是一级还是二级 // 判断是选中还是取消选中 if ( e.target.parentNode.getAttribute("aria-checked") === "true" ) { if ( allDataPool[e.target.innerHTML] && allDataPool[e.target.innerHTML].with ) { return; } // 取消选中 if (e.target.parentNode.childNodes.length === 2) { // with的判断不光是选中这项,还有判断当前选中这项有没有被其他的项with let withItem = getWithItem(e.target.innerHTML); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData(e.target.innerHTML, withItem, false); let data = handleDataSourceData(e.target.innerHTML, 1); reduxDispatch(getDataSourceChangeAction(data)); // console.log("点击的是文字,取消", e.target.innerHTML); } } else { if ( allDataPool[e.target.innerHTML] && allDataPool[e.target.innerHTML].with ) { return; } // 选中 if (e.target.parentNode.childNodes.length === 2) { let withItem = getWithItem(e.target.innerHTML); // 有其他通过with关联选中项的数据都要进行选中处理 handleWithData(e.target.innerHTML, withItem, true); let data = handleDataSourceData(e.target.innerHTML, -1); reduxDispatch(getDataSourceChangeAction(data)); //console.log("点击的是文字,选中", e.target.innerHTML); } } } }} multiple="multiple" maxTagCount="responsive" />
{ // form.setFieldsValue({ // [`${info.ip}`]: value, // }); // }} > 选择服务数:{" "} {value.length == 0 ? ( 0个 ) : (
已选服务
{value.map((item) => { return (
{`${item[0]} / ${item[1]}`}
); })}
} > {value.length}个 )}
已安装服务数:{" "} {info.num == 0 ? ( 0个 ) : ( {info.num}个 )}
); }; export default ServiceDistributionItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/component/index.module.less ================================================ .basicInfoItem { display: flex; margin-top: 25px; align-items: center; } .dependentinfoItem { display: flex; margin-top: 35px; align-items: center; } :global{ .ant-form-vertical .ant-form-item-label, .ant-col-24.ant-form-item-label, .ant-col-xl-24.ant-form-item-label { padding:0 } } ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/index.js ================================================ import { useHistory, useLocation } from "react-router-dom"; import { useDispatch } from "react-redux"; import { Steps } from "antd"; import { useState } from "react"; import styles from "./index.module.less"; import Step1 from "./steps/Step1"; import Step2 from "./steps/Step2"; import Step3 from "./steps/Step3"; import Step4 from "./steps/Step4"; import { LeftOutlined } from "@ant-design/icons"; // 安装页面 const Installation = () => { const dispatch = useDispatch(); const history = useHistory(); const location = useLocation() const defaultStep = location.state?.step //console.log(location, defaultStep) const [stepNum, setStepNum] = useState(defaultStep || 0); return (
{ // dispatch(getTabKeyChangeAction("service")); history?.goBack(); // history?.push({ // pathname: `/application_management/app_store`, // }); }} /> 安装
{stepNum == 0 && } {stepNum == 1 && } {stepNum == 2 && } {stepNum == 3 && }
); }; export default Installation; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/index.module.less ================================================ .backIcon:hover { color: rgb(46, 124, 238); } :global { .ant-anchor-ink::before { background-color: #fff!important; } } ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/steps/Step1.js ================================================ import BasicInfoItem from "../component/BasicInfoItem/index"; import DependentInfoItem from "../component/DependentinfoItem/index"; import { Form, Button, message } from "antd"; import { useEffect, useState } from "react"; import { fetchPost } from "@/utils/request"; import { useSelector, useDispatch } from "react-redux"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { getStep1ChangeAction } from "../store/actionsCreators"; import * as R from "ramda"; const Step1 = ({ setStepNum }) => { const data = useSelector((state) => state.installation.step1Data); const reduxDispatch = useDispatch(); const [loading, setLoading] = useState(false); const uniqueKey = useSelector((state) => state.appStore.uniqueKey); // 定义下一步是否可操作,只在第一次加载时判断 const [isContinue, setIsContinue] = useState(false); // 基本信息的form实例 const [basicForm] = Form.useForm(); // 依赖信息的form实例 const [dependentForm] = Form.useForm(); const dataProcessing = () => { let formBasicData = basicForm.getFieldsValue(); let formDependentData = dependentForm.getFieldsValue(); //setStepNum(1); let basic = JSON.parse(JSON.stringify(data.basic)); let dependent = JSON.parse(JSON.stringify(data.dependence)); basic = basic.map((item) => { let services_list = item.services_list; let cluster_name = ""; Object.keys(formBasicData).map((k) => { let kArr = k.split("="); if (kArr.length == 1) { // 长度为1 说明当前key就是实例名称 // console.log(k, formBasicData[k]); if (k == item.name) { cluster_name = formBasicData[k]; } } else if (kArr.length == 2) { services_list = services_list.map((i) => { // console.log(i); if (i.name == kArr[1]) { return { name: i.name, version: i.version, deploy_mode: Number(formBasicData[k]), }; } else { return { ...i, }; } }); } }); return { name: item.name, version: item.version, cluster_name: cluster_name, services_list: services_list, }; }); dependent = dependent.map((item) => { if (item.is_base_env) { // jdk return { ...item, }; } else { //if(item.is_use_exist){ // deployInstanceRow let exist_instance = item.exist_instance; let deploy_mode = item.deploy_mode; let cluster_name = ""; let vip = ""; let is_use_exist = false; Object.keys(formDependentData).map((k) => { let kArr = k.split("="); if (kArr[0] == item.name) { if (kArr.length == 1) { // 选中了勾选了说明当前为选择实例信息 exist_instance = JSON.parse(formDependentData[k]); is_use_exist = true; } else { // 取消了选中,当前为部署数量信息 // 判断部署数量是否是数字 if (kArr[1] == "num") { // deploy_mode = formDependentData[k]; cluster_name = formDependentData[`${item.name}=name`]; vip = formDependentData[`${item.name}=vip`]; if (isNaN(Number(formDependentData[k]))) { // 非数字代表单实例,主从,主主 deploy_mode = formDependentData[k]; } else { // 数字代表部署数量 deploy_mode = Number(formDependentData[k]); // 数量 //cluster_name } } } } }); return { ...item, exist_instance: exist_instance, deploy_mode: deploy_mode, cluster_name: cluster_name, vip: vip, is_use_exist: is_use_exist, }; } return { ...item, }; }); return { basic: basic, dependence: dependent, }; }; // const reduxDataProcessing = () => { // let formBasicData = basicForm.getFieldsValue(); // let formDependentData = dependentForm.getFieldsValue(); // //setStepNum(1); // let basic = JSON.parse(JSON.stringify(data.basic)); // let dependent = JSON.parse(JSON.stringify(data.dependence)); // basic = basic.map((item) => { // let services_list = item.services_list; // let cluster_name = ""; // Object.keys(formBasicData).map((k) => { // let kArr = k.split("="); // if (kArr.length == 1) { // // 长度为1 说明当前key就是实例名称 // // console.log(k, formBasicData[k]); // if (k == item.name) { // cluster_name = formBasicData[k]; // } // } else if (kArr.length == 2) { // services_list = services_list.map((i) => { // // console.log(i); // if (i.name == kArr[1]) { // return { // name: i.name, // version: i.version, // deploy_mode: { // ...i.deploy_mode, // default: Number(formBasicData[k]), // } // }; // } else { // return { // ...i, // }; // } // }); // } // }); // return { // name: item.name, // version: item.version, // cluster_name: cluster_name, // services_list: services_list, // }; // }); // console.log(data.dependence) // dependent = dependent.map((item) => { // if (item.is_base_env) { // // jdk // return { // ...item, // }; // } else { // //if(item.is_use_exist){ // // deployInstanceRow // let exist_instance = item.exist_instance; // let deploy_mode = item.deploy_mode; // let cluster_name = ""; // let vip = ""; // let is_use_exist = false; // Object.keys(formDependentData).map((k) => { // let kArr = k.split("="); // if (kArr[0] == item.name) { // if (kArr.length == 1) { // // 选中了勾选了说明当前为选择实例信息 // //console.log(formDependentData[k]) // exist_instance = JSON.parse(formDependentData[k]); // is_use_exist = true; // } else { // // 取消了选中,当前为部署数量信息 // // 判断部署数量是否是数字 // if (kArr[1] == "num") { // // deploy_mode = formDependentData[k]; // cluster_name = formDependentData[`${item.name}=name`]; // vip = formDependentData[`${item.name}=vip`]; // if (isNaN(Number(formDependentData[k]))) { // // 非数字代表单实例,主从,主主 // console.log("非数字", formDependentData[k],item) // deploy_mode = formDependentData[k]; // } else { // console.log("数字", formDependentData[k]) // // 数字代表部署数量 // deploy_mode = Number(formDependentData[k]); // // 数量 // //cluster_name // } // } // } // } // }); // console.log(exist_instance, deploy_mode, cluster_name, vip, is_use_exist,) // console.log(item.exist_instance) // let processedExistInstance = [] // if(typeof exist_instance == "object"){ // processedExistInstance = item.exist_instance.map(m=>{ // if(m.id == exist_instance){ // return { // ...exist_instance, // isCheck: true // } // }else{ // return m // } // }) // } // return { // ...item, // exist_instance: processedExistInstance, // deploy_mode: { // ...item.deploy_mode, // default:deploy_mode && typeof deploy_mode == "number"? deploy_mode:deploy_mode.default, // }, // cluster_name: cluster_name, // vip: vip, // is_use_exist: is_use_exist, // }; // } // return { // ...item, // }; // }); // return { // basic: basic, // dependence: dependent, // is_continue: true // }; // }; // checkInstallInfo 基本信息提交操作,决定是否跳转服务分布以及数据校验回显 const checkInstallInfo = (queryData) => { // console.log(uniqueKey,data) // return setLoading(true); fetchPost(apiRequest.appStore.checkInstallInfo, { body: { unique_key: uniqueKey, data: queryData, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { if (res.data.data.is_continue) { // 校验通过,跳转,请求服务分布数据并跳转 setStepNum(1); } else { message.warn("校验未通过"); // 当校验未通过时不跳转,并回显数据 // 使用redux中已有的数据做基础,在其上添加失败的校验信息 let { basic, dependence } = R.clone(data); let { basic: resBasic, dependence: resDependence } = R.clone( res.data.data ); reduxDispatch( getStep1ChangeAction({ basic: basic.map((item) => { let d = R.clone(item); resBasic.map((i) => { if (i.error_msg) { if (d.name == i.name) { d.error_msg = i.error_msg; } } }); return d; }), dependence: dependence.map((item) => { let d = R.clone(item); resDependence.map((i) => { if (i.error_msg) { if (d.name == i.name) { d.error_msg = i.error_msg; } } }); return d; }), }) ); } } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { setIsContinue(data.is_continue); }, []); console.log("渲染数据", data); // 组件渲染特殊处理 if (data.basic.length == 0) { return ( <>
基本信息
0 ? data.dependence[0] : { deploy_mode: {}, } } />
依赖信息
{data.dependence.filter((item, idx) => idx !== 0).length == 0 ? ( "无" ) : (
{data.dependence .filter((item, idx) => idx !== 0) .map((item) => { return ( ); })} )}
); } else { return ( <>
基本信息
{data.basic.map((item) => { return ( ); })}
依赖信息
{data.dependence.map((item) => { return ( ); })}
); } }; export default Step1; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/steps/Step2.js ================================================ import { Button, Form, Spin, message } from "antd"; import { useEffect, useState } from "react"; import ServiceDistributionItem from "../component/ServiceDistributionItem/index.js"; import { useSelector, useDispatch } from "react-redux"; import { getDataSourceChangeAction, getStep2ErrorLstChangeAction, getIpListChangeAction, } from "../store/actionsCreators"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; const Step2 = ({ setStepNum }) => { const reduxDispatch = useDispatch(); const uniqueKey = useSelector((state) => state.appStore.uniqueKey); // 基本信息的form实例 const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [installService, setInstallService] = useState({}); const allDataPool = useSelector((state) => state.installation.dataSource); const ipList = useSelector((state) => state.installation.ipList); const [data, setData] = useState({ host: [], product: [], }); // 未分配服务个数 const unassignedServices = Object.keys(allDataPool).reduce((prev, cur) => { return prev + allDataPool[cur].num; }, 0); const queryCreateServiceDistribution = () => { // checkInstallInfo 基本信息提交操作,决定是否跳转服务分布以及数据校验回显 setLoading(true); fetchPost(apiRequest.appStore.createServiceDistribution, { body: { unique_key: uniqueKey, }, }) .then((res) => { handleResponse(res, (res) => { if (res.data && res.data.data) { reduxDispatch(getDataSourceChangeAction(res.data.data.all)); setData((data) => { return { ...data, host: res.data.data.host, product: res.data.data.product, }; }); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 已安装服务列表查询 const queryInstallServiceData = () => { fetchGet(apiRequest.appStore.queryListServiceByIp) .then((res) => { handleResponse(res, (res) => { setInstallService(res.data); }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 提交前对数据进行处理 const dataProcessing = () => { let data = form.getFieldValue(); console.log(data); let result = {}; for (const key in data) { if (data[key].length > 0) { result[key] = data[key].map((item) => { return item[1]; }); } } console.log(result); return result; }; // 最后的提交操作 // checkServiceDistribution 服务分布提交操作,决定是否跳转修改配置以及数据校验回显 const checkServiceDistribution = (queryData) => { setLoading(true); fetchPost(apiRequest.appStore.checkServiceDistribution, { body: { unique_key: uniqueKey, data: queryData, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { if (res.data.is_continue) { // 校验通过,跳转,请求服务分布数据并跳转 setStepNum(2); } else { message.warn("校验未通过"); reduxDispatch(getStep2ErrorLstChangeAction(res.data.error_lst)); } } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { queryCreateServiceDistribution(); queryInstallServiceData(); return () => { // 销毁时去除error信息 reduxDispatch(getStep2ErrorLstChangeAction([])); reduxDispatch(getDataSourceChangeAction([])); reduxDispatch(getIpListChangeAction([])); }; }, []); return ( <>
主机总数: {data.host.length}台
未分配服务: {unassignedServices}个
{data.host.map((item) => { return ( ); })}
已分配主机数量: {ipList?.length}台
); }; export default Step2; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/steps/Step3.js ================================================ import { Button, Form, Checkbox, Input, Spin, message } from "antd"; import { useEffect, useState } from "react"; import { SearchOutlined, ExclamationOutlined } from "@ant-design/icons"; import { useSelector, useDispatch } from "react-redux"; import ServiceConfigItem from "../component/ServiceConfigItem"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { getStep3IpDataChangeAction, getStep3ErrorInfoChangeAction, getStep3ServiceChangeAction, } from "../store/actionsCreators"; import * as R from "ramda"; const Step3 = ({ setStepNum }) => { const dispatch = useDispatch(); // unique_key: "21e041a9-c9a5-4734-9673-7ed932625d21" // 服务的loading const [loading, setLoading] = useState(false); const [loadingIp, setLoadingIp] = useState(false); const uniqueKey = useSelector((state) => state.appStore.uniqueKey); // redux中取数据 const reduxData = useSelector((state) => state.installation.step3Data); const errInfo = useSelector((state) => state.installation.step3ErrorData); //console.log(errInfo["10.0.12.250"]); const [checked, setChecked] = useState(false); const [serviceConfigform] = Form.useForm(); const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 指定本次安装服务运行用户 const [runUser, setRunUser] = useState(""); // 筛选后的ip列表 const [currentIpList, setCurrentIpList] = useState([]); // ip列表中的选中项 const [checkIp, setCheckIp] = useState(""); // ip 筛选value const [searchIp, setSearchIp] = useState(""); // ip列表的数据源 const [ipList, setIpList] = useState([]); function queryIpList() { setLoadingIp(true); fetchGet(apiRequest.appStore.getInstallHostRange, { params: { unique_key: uniqueKey, }, }) .then((res) => { handleResponse(res, (res) => { setIpList(res.data.data); setCurrentIpList(res.data.data); setCheckIp(res.data.data[0]); }); }) .catch((e) => console.log(e)) .finally(() => { setLoadingIp(false); }); } const queryInstallArgsByIp = (ip) => { // 如果redux中已经存了当前ip的数据就不再请求直接使用redux中的 if (reduxData[ip]) { return; } setLoading(true); fetchGet(apiRequest.appStore.getInstallArgsByIp, { params: { unique_key: uniqueKey, ip: ip, }, }) .then((res) => { handleResponse(res, (res) => { //setDataSource(res.data.data); dispatch( getStep3IpDataChangeAction({ [ip]: res.data.data, }) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 提交前对数据进行处理 const dataProcessing = (data) => { let obj = {}; ipList.map((ip) => { obj[ip] = []; }); //console.log(data); return { ...obj, ...data, }; }; // 有校验失败的信息生成errlist const getErrorInfo = (data) => { let result = {}; for (const ip in data) { data[ip].map((service) => { [...service.install_args, ...service.ports].map((serv) => { if (serv.check_flag == false) { if (!result[ip]) { result[ip] = {}; } if (result[ip][service.name]) { result[ip][service.name][serv.key] = serv.error_msg; } else { result[ip][service.name] = {}; result[ip][service.name][serv.key] = serv.error_msg; } } }); }); } // console.log(result, "result"); return result; }; // 非空校验 const nonNullCheck = (queryData) => { let result = {}; //console.log(queryData); let data = R.clone(queryData); // 这一步校验是为了解决非当前页面form的必填校验不到的问题 // 当key == vip 跳过校验(非必填) // 首先去除当前页面ip的相关数据校验 data[checkIp] = []; // console.log(data) for (const ip in data) { data[ip].map((service) => { [...service.install_args, ...service.ports].map((serv) => { // console.log(serv); // 特殊处理,去除vip非空校验 if (!serv.default && serv.key !== "vip") { if (!result[ip]) { result[ip] = {}; } if (result[ip][service.name]) { result[ip][service.name][serv.key] = `请输入${serv.name}`; } else { result[ip][service.name] = {}; result[ip][service.name][serv.key] = `请输入${serv.name}`; } } }); }); } return result; }; const getCurrentData = (queryData) => { let formData = serviceConfigform.getFieldValue(); for (const keyStr in formData) { let keyArr = keyStr.split("="); queryData[checkIp] = queryData[checkIp].map((item) => { if (item.name == keyArr[0]) { let obj = { ...item }; obj.install_args = obj.install_args.map((i) => { if (i.key == keyArr[1]) { i.default = formData[keyStr]; } return i; }); obj.ports = obj.ports.map((i) => { if (i.key == keyArr[1]) { i.default = formData[keyStr]; } return i; }); return obj; } return item; }); } return queryData; }; // 开始安装操作命令下发 const createInstallPlan = (queryData) => { // 这个queryData数据是用redux中来的,当前页面的数据是在页面销毁时才同步redux的 console.log(getCurrentData(queryData)); //return setLoading(true); fetchPost(apiRequest.appStore.createInstallPlan, { body: { unique_key: uniqueKey, run_user: runUser, data: getCurrentData(queryData), }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { if (res.data.is_continue) { // 校验通过,跳转,请求服务分布数据并跳转 setStepNum(3); } else { res.data && res.data.error_msg ? message.warn(res.data.error_msg) : message.warn("校验未通过,请检查"); dispatch( getStep3ErrorInfoChangeAction(getErrorInfo(res.data.data)) ); //reduxDispatch(getStep2ErrorLstChangeAction(res.data.error_lst)); } } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { if (checkIp) { queryInstallArgsByIp(checkIp); } }, [checkIp]); useEffect(() => { // 请求ip数据 // currentIpList queryIpList(); return () => { dispatch(getStep3ErrorInfoChangeAction({})); dispatch(getStep3IpDataChangeAction()); }; }, []); return (
{ if (!e.target.checked) { setRunUser(""); } setChecked(e.target.checked); }} > 指定本次安装服务运行用户
{ setRunUser(e.target.value); }} />
{ if (searchIp) { let result = ipList.filter((i) => { return i.includes(searchIp); }); setCurrentIpList(result); result.length > 0 && setCheckIp(result[0]); } else { setCurrentIpList(ipList); setCheckIp(ipList[0]); } }} value={searchIp} onChange={(e) => { setSearchIp(e.target.value); if (!e.target.value) { setCurrentIpList(ipList); setCheckIp(ipList[0]); } }} onPressEnter={() => { if (searchIp) { let result = ipList.filter((i) => { return i.includes(searchIp); }); setCurrentIpList(result); result.length > 0 && setCheckIp(result[0]); } else { setCurrentIpList(ipList); setCheckIp(ipList[0]); } }} placeholder="搜索IP地址" suffix={ !searchIp && } />
{currentIpList.length == 0 ? (
) : ( <> {currentIpList?.map((item) => { return (
{ // 点击切换ip时再存redux,避免不必要的性能消耗 let formData = serviceConfigform.getFieldValue(); for (const key in formData) { let keyArr = key.split("="); reduxData[checkIp].map((item) => { if (keyArr[0] == item.name) { dispatch( getStep3ServiceChangeAction( checkIp, item.name, keyArr[1], formData[key] ) ); } }); } setCheckIp(item); }} > {item}
{errInfo[item] && (
)}
); })} )}
{!reduxData[checkIp] || reduxData[checkIp].length == 0 ? (
) : (
{reduxData[checkIp]?.map((item, idx) => { return ( ); })} )}
分布主机数量: {ipList?.length}台
); }; export default Step3; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/steps/Step4.js ================================================ import { Button, Anchor, Spin, Progress } from "antd"; import { useSelector } from "react-redux"; import InstallInfoItem from "../component/InstallInfoItem"; import { useEffect, useRef, useState } from "react"; import { LoadingOutlined } from "@ant-design/icons"; import { apiRequest } from "@/config/requestApi"; import { fetchGet } from "@/utils/request"; import { handleResponse } from "@/utils/utils"; import { useHistory, useLocation } from "react-router-dom"; import { fetchPost } from "src/utils/request"; const { Link } = Anchor; // 状态渲染规则 const renderStatus = { 0: "等待安装", 1: "正在安装", 2: "安装成功", 3: "安装失败", 4: "正在注册", }; const Step4 = () => { const history = useHistory(); const viewHeight = useSelector((state) => state.layouts.viewSize.height); const [openName, setOpenName] = useState(""); // 在轮训时使用ref存值 const openNameRef = useRef(null); const location = useLocation(); const defaultUniqueKey = location.state?.uniqueKey; const uniqueKey = useSelector((state) => state.appStore.uniqueKey); const [loading, setLoading] = useState(true); const [retryLoading, setRetryLoading] = useState(false); // 主机 agent 状态标识 const [hostAgentFlag, setHostAgentFlag] = useState(false); // 轮训控制器 const hostAgentTimer = useRef(null); // start 按钮加载 const [startLoading, setStartLoading] = useState(false); const [data, setData] = useState({ detail: {}, status: 0, }); const [log, setLog] = useState(""); // 轮训的timer控制器 const timer = useRef(null); const queryInstallProcess = () => { !timer.current && setLoading(true); fetchGet(apiRequest.appStore.queryInstallProcess, { params: { unique_key: defaultUniqueKey || uniqueKey, }, }) .then((res) => { handleResponse(res, (res) => { setData(res.data); if ( res.data.status == 0 || res.data.status == 1 || res.data.status == 4 ) { // 状态为未安装或者安装中 if (openNameRef.current) { let arr = openNameRef.current.split("="); queryDetailInfo(defaultUniqueKey || uniqueKey, arr[1], arr[0]); } timer.current = setTimeout(() => { queryInstallProcess(); }, 5000); } else { if (openNameRef.current) { let arr = openNameRef.current.split("="); queryDetailInfo(defaultUniqueKey || uniqueKey, arr[1], arr[0]); } } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 请求详细信息 const queryDetailInfo = (uniqueKey, ip, app_name) => { fetchGet(apiRequest.appStore.showSingleServiceInstallLog, { params: { unique_key: uniqueKey, app_name: app_name, ip: ip, }, }) .then((res) => { handleResponse(res, (res) => { setLog(res.data.log); }); }) .catch((e) => console.log(e)) .finally(() => { //setLoading(false); }); }; const retryInstall = () => { setRetryLoading(true); setOpenName(""); openNameRef.current = ""; fetchPost(apiRequest.appStore.retryInstall, { body: { unique_key: defaultUniqueKey || uniqueKey, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { queryInstallProcess(); } }); }) .catch((e) => console.log(e)) .finally(() => { setRetryLoading(false); }); }; // 查询主机 agent 状态 const queryHostAgent = () => { // 构造 ip 集合 let ipSet = new Set(); Object.keys(data.detail).map((key, idx) => { data.detail[key].forEach((e) => { if (e.ip !== "postAction") { ipSet.add(e.ip); } }); }); fetchPost(apiRequest.machineManagement.hostsAgentStatus, { body: { ip_list: Array.from(ipSet), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code === 0 && res.data) { // 调用安装 retryInstall(); // 清除定时器 clearInterval(hostAgentTimer.current); setStartLoading(false); } }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 开始安装 const startInstall = () => { setStartLoading(true); hostAgentTimer.current = setInterval(() => { queryHostAgent(); }, 1000); }; useEffect(() => { queryInstallProcess(); return () => { // 页面销毁时清除延时器 clearTimeout(timer.current); clearInterval(hostAgentTimer.current); }; }, []); return (
{Object.keys(data.detail).map((key, idx) => { return ( { setLog(""); if (n) { let arr = n.split("="); queryDetailInfo( defaultUniqueKey || uniqueKey, arr[1], arr[0] ); } // console.log(n); // queryDetailInfo(uniqueKey, arr[1], arr[0]); setOpenName(n); openNameRef.current = n; }} id={`a${key}`} key={key} title={key} data={data.detail[key]} log={log} idx={idx} /> ); })}
{ let con = document.getElementById("Step4Wrapper"); return con; }} style={{ height: viewHeight - 270, overflowY: "auto", }} onClick={(e) => { e.preventDefault(); }} > {Object.keys(data.detail).map((key) => { // console.log(data.detail[key]); let hasError = data.detail[key].filter((a) => a.status == 3).length !== 0; return (
{key} } />
); })}
{renderStatus[data.status]} {(data.status == 0 || data.status == 1 || data.status == 4) && ( )}
{data.status == 0 && ( )} {data.status == 3 && ( )}
); }; export default Step4; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/store/actionsCreators.js ================================================ import * as actionTypes from "./constants"; import * as R from "ramda"; export const getDataSourceChangeAction = (value) => { const dataSource = R.clone(value); return { type: actionTypes.CHANGE_DATASOURCE, payload: { dataSource: dataSource, }, }; }; export const getIpListChangeAction = (value) => { const list = R.clone(value); return { type: actionTypes.CHANGE_IPLIST, payload: { ipList: list, }, }; }; export const getStep1ChangeAction = (value) => { const data = R.clone(value); return { type: actionTypes.CHANGE_STEP1DATA, payload: { step1Data: data, }, }; }; export const getStep2ChangeAction = (value) => { const data = R.clone(value); return { type: actionTypes.CHANGE_STEP2DATA, payload: { step2Data: data, }, }; }; export const getStep2ErrorLstChangeAction = (value) => { const data = R.clone(value); return { type: actionTypes.CHANGE_STEP2ERRORLISTDATA, payload: { errorList: data, }, }; }; export const getStep3IpDataChangeAction = (value) => { const data = R.clone(value); return { type: actionTypes.CHANGE_STEP3DATA, payload: { ipData: data, }, }; }; export const getStep3ServiceChangeAction = (ip, name, key, value) => { return { type: actionTypes.CHANGE_STEP3SERVERDATA, payload: { ip, name, key, value, }, }; }; export const getStep3ErrorInfoChangeAction = (value) => { console.log(value) return { type: actionTypes.CHANGE_STEP3ERRORDATA, payload: { err: value, }, }; }; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/store/constants.js ================================================ export const CHANGE_DATASOURCE = "CHANGE_DATASOURCE"; export const CHANGE_IPLIST = "CHANGE_IPLIST"; export const CHANGE_STEP1DATA = "CHANGE_STEP1DATA"; export const CHANGE_STEP2DATA = "CHANGE_STEP2DATA"; export const CHANGE_STEP2ERRORLISTDATA = "CHANGE_STEP2ERRORLISTDATA"; export const CHANGE_STEP3DATA = "CHANGE_STEP3DATA"; export const CHANGE_STEP3SERVERDATA = "CHANGE_STEP3SERVERDATA"; export const CHANGE_STEP3ERRORDATA = "CHANGE_STEP3ERRORDATA"; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/store/index.js ================================================ import reducer from "./reduer"; export { reducer }; ================================================ FILE: omp_web/src/pages/AppStore/config/Installation/store/reduer.js ================================================ import * as actionTypes from "./constants"; import * as R from "ramda"; const defaultState = { dataSource: {}, ipList: [], step1Data: { basic: [], dependence: [], }, step2Data: {}, errorList: [], step3Data: {}, // step3Data数据结构 // { // ip10.012.1:[{ // name:doucZabbixApi // log_dir:/abc/d, // ... // }, // ...] // } step3ErrorData: {}, }; function reducer(state = defaultState, action) { const stateCopy = R.clone(state); switch (action.type) { case actionTypes.CHANGE_DATASOURCE: return { ...state, dataSource: action.payload.dataSource }; case actionTypes.CHANGE_IPLIST: return { ...state, ipList: action.payload.ipList }; case actionTypes.CHANGE_STEP1DATA: return { ...state, step1Data: action.payload.step1Data }; case actionTypes.CHANGE_STEP2DATA: return { ...state, step2Data: action.payload.step2Data }; case actionTypes.CHANGE_STEP2ERRORLISTDATA: return { ...state, errorList: action.payload.errorList }; case actionTypes.CHANGE_STEP3DATA: if(!action.payload.ipData){ return { ...state, step3Data:{} } } return { ...state, step3Data: { ...state.step3Data, ...action.payload.ipData, }, }; case actionTypes.CHANGE_STEP3SERVERDATA: let { ip, name, key, value } = action.payload; let serviceData = stateCopy.step3Data[ip]; serviceData = serviceData.map((item) => { if (item.name == name) { let obj = { ...item }; obj.install_args = obj.install_args.map((i) => { if (i.key == key) { return { ...i, default: value, }; } return i; }); obj.ports = obj.ports.map((i) => { if (i.key == key) { return { ...i, default: value, }; } return i; }); return { ...obj, }; } return item; }); return { ...stateCopy, step3Data: { ...stateCopy.step3Data, [ip]: serviceData, }, }; case actionTypes.CHANGE_STEP3ERRORDATA: return { ...stateCopy, step3ErrorData: action.payload.err, }; default: return state; } } export default reducer; ================================================ FILE: omp_web/src/pages/AppStore/config/ReleaseModal.js ================================================ import { Button, Modal, Upload, message, Steps, Tooltip } from "antd"; import { useEffect, useRef, useState } from "react"; import { CloudUploadOutlined, ExclamationCircleOutlined, CloseCircleFilled, CheckCircleFilled, SyncOutlined, ClockCircleFilled, SendOutlined, } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { OmpMessageModal } from "@/components"; //let bmf = new BMF(); const ReleaseModal = ({ setReleaseModalVisibility, releaseModalVisibility, timeUnix, refresh, }) => { const [loading, setLoading] = useState(false); const [filesList, setFilesList] = useState([]); const [deleteModal, setDeleteModal] = useState(false); const [dataSource, setDataSource] = useState([]); const [stepNum, setStepNum] = useState(0); const isRun = useRef(null); const timer = useRef(null); // 失败时的轮训次数标识 const trainingInRotationNum = useRef(0); function checkData() { // 防止在弹窗关闭后还继续轮训 if (!isRun.current) { return; } fetchGet(apiRequest.appStore.pack_verification_results, { params: { operation_uuid: timeUnix, }, }) .then((res) => { if (res) handleResponse(res, (res) => { setDataSource(res.data); if (res.data) { let checkRunningArr = res.data.filter((item) => { return item.package_status == 2; }); let publishRunningArr = res.data.filter((item) => { return item.package_status == 5; }); if (checkRunningArr.length > 0 || publishRunningArr.length > 0) { timer.current = setTimeout(() => { checkData(); }, 2000); } } }); }) .catch((e) => { console.log(e); trainingInRotationNum.current++; if (trainingInRotationNum.current < 3) { setTimeout(() => { checkData(); }, 5000); } else { setDataSource((data) => { console.log(data); return data.map((item) => { return { ...item, package_status: 9, }; }); }); } }) .finally((e) => {}); } // 发布的查询 function publishCheckData() { // 防止在弹窗关闭后还继续轮训 if (!isRun.current) { return; } fetchGet(apiRequest.appStore.publish, { params: { operation_uuid: timeUnix, }, }) .then((res) => { if (res) handleResponse(res, (res) => { setDataSource(res.data); if (res.data) { let checkRunningArr = res.data.filter((item) => { return item.package_status == 2; }); let publishRunningArr = res.data.filter((item) => { return item.package_status == 5; }); if (checkRunningArr.length > 0 || publishRunningArr.length > 0) { setTimeout(() => { publishCheckData(); }, 2000); } } }); }) .catch((e) => { console.log(e); trainingInRotationNum.current++; if (trainingInRotationNum.current < 3) { setTimeout(() => { publishCheckData(); }, 5000); } else { setDataSource((data) => { //console.log(data); return data.map((item) => { return { ...item, package_status: 9, }; }); }); } }) .finally((e) => {}); } // 发布 const publishApp = () => { setLoading(true); fetchPost(apiRequest.appStore.publish, { body: { uuid: timeUnix, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 0) { setStepNum(2); publishCheckData(); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { isRun.current = releaseModalVisibility; }, [releaseModalVisibility]); return ( <> 发布 } afterClose={() => { clearTimeout(timer.current); setFilesList([]); setStepNum(0); setDataSource([]); refresh(); }} onCancel={() => { if ( stepNum == 2 && dataSource.filter((item) => item.package_status == 5).length == 0 ) { setReleaseModalVisibility(false); return; } if (filesList.length == 0) { setReleaseModalVisibility(false); } else { setDeleteModal(true); } }} visible={releaseModalVisibility} footer={null} width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose > {stepNum == 0 && (
{ //console.log(arg,file) return { uuid: timeUnix, operation_user: localStorage.getItem("username"), md5: file.uid, }; }} maxCount={5} beforeUpload={(file, fileList) => { const fileSize = file.size / 1024 / 1024 / 1024; //单位是G if (Math.ceil(fileSize) > 4) { message.error("仅支持传入4G以内文件"); return Upload.LIST_IGNORE; } if (fileList.length > 5) { if (file == fileList[0]) { message.error("单次发布操作,最多支持上传5个文件"); } return Upload.LIST_IGNORE; } if (filesList.length + fileList.length > 5) { if (file == fileList[0]) { message.error("单次发布操作,最多支持上传5个文件"); } return Upload.LIST_IGNORE; } if (filesList.length >= 5) { if (file == fileList[0]) { message.error("单次发布操作,最多支持上传5个文件"); } return Upload.LIST_IGNORE; } var reg = /[^a-zA-Z0-9\_\-\.]/g; if (reg.test(file.name)) { message.error(`文件名仅支持字母、数字、"_"、"-"和"."`); return Upload.LIST_IGNORE; } let fileNameArr = fileList.length == 1 ? [...filesList, file].map((i) => i.name) : [...filesList, ...fileList].map((i) => i.name); let uniqueArr = [...new Set(fileNameArr)]; if (fileNameArr.length !== uniqueArr.length) { if (fileList[0].uid == file.uid) { message.error("上传文件存在同名"); } return Upload.LIST_IGNORE; } }} onChange={(e) => { setFilesList(e.fileList); }} onRemove={(e) => { if (e && e.response && e.response.code == 0) { fetchPost(apiRequest.appStore.remove, { body: { uuid: timeUnix, package_names: [e.name], }, }) .then((res) => { handleResponse(res, (res) => {}); }) .catch((e) => console.log(e)) .finally(() => {}); } }} >

点击或将文件拖拽到这里上传

支持扩展名: .tar 或 .tar.gz 文件大小不超过4G

最多上传5个文件

)} {stepNum == 1 && (

校验失败的安装包不会在应用商店创建,如相同名称应用已存在,发布后会替换原有安装包

{dataSource.map((item, idx) => { return (
{item.package_name}
{item.package_status == 0 && ( 校验通过 )} {item.package_status == 1 && ( 校验失败 )} {item.package_status == 2 && ( 校验中... )} {item.package_status == 9 && ( 网络错误 )}
{item.error_msg}
); })}
)} {stepNum == 2 && (
{dataSource.filter((item) => item.package_status == 5).length > 0 ? (

发布中

) : (

发布完成

本次成功发布{" "} {dataSource.filter((item) => item.package_status == 3).length} 个 服务

发布完成的安装包存放路径:{" "} omp/package_hub/verified/ {" "}

)} {dataSource.map((item, idx) => { return (
{item.package_name}
{item.package_status == 3 && ( 发布成功 )} {item.package_status == 4 && ( 发布失败 )} {item.package_status == 5 && ( 发布中... )} {item.package_status == 9 && ( 网络错误 )}
{item.error_msg}
); })}
)}
提示 } //loading={loading} onFinish={() => { let updatedFile = filesList.filter( (i) => i.response && i.response.code == 0 ); if (updatedFile.length == 0) { setReleaseModalVisibility(false); setDeleteModal(false); return; } fetchPost(apiRequest.appStore.remove, { body: { uuid: timeUnix, package_names: updatedFile.map((i) => i.name), }, }) .then((res) => { handleResponse(res, (res) => {}); }) .catch((e) => console.log(e)) .finally(() => { setReleaseModalVisibility(false); setDeleteModal(false); }); }} >

确认要中断发布操作吗 ?

正在进行发布操作,关闭窗口将会中断,此操作不可逆。

); }; export default ReleaseModal; ================================================ FILE: omp_web/src/pages/AppStore/config/Rollback/content/component/RollbackDetail.js ================================================ import { DownOutlined } from "@ant-design/icons"; import { useEffect, useRef, useState } from "react"; import { OmpToolTip } from "@/components"; const stepOpen = { marginTop: 10, minHeight: 30, height: 300, transition: "all .2s ease-in", //overflow: "hidden", backgroundColor: "#000", color: "#fff", padding: 10, overflowY: "auto", whiteSpace: "pre-line", }; const stepNotOpen = { height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }; // 状态渲染规则 const renderStatus = { 0: 等待回滚, 1: 正在回滚, 2: 回滚成功, 3: 回滚失败, }; const RollbackDetail = ({ title, ip, status, log, instance_name }) => { const containerRef = useRef(null); const [openName, setOpenName] = useState(""); useEffect(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight; }, [log]); return ( ); }; export default RollbackDetail; ================================================ FILE: omp_web/src/pages/AppStore/config/Rollback/content/component/RollbackInfoItem.js ================================================ import RollbackDetail from "./RollbackDetail"; const RollbackInfoItem = ({ id, data, title, log, idx, instance_name }) => { return (
{title}
{data?.map((item) => { console.log(item); return ( ); })}
); }; export default RollbackInfoItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Rollback/content/index.js ================================================ import { Button, Anchor, Spin, Progress } from "antd"; import { useSelector } from "react-redux"; import RollbackInfoItem from "./component/RollbackInfoItem"; import { useEffect, useRef, useState } from "react"; import { LoadingOutlined } from "@ant-design/icons"; import { apiRequest } from "@/config/requestApi"; import { fetchGet } from "@/utils/request"; import { handleResponse } from "@/utils/utils"; import { useHistory, useLocation } from "react-router-dom"; import { fetchPut } from "src/utils/request"; const { Link } = Anchor; // 状态渲染规则 const renderStatus = { 0: "等待回滚", 1: "正在回滚", 2: "回滚成功", 3: "回滚失败", 4: "正在注册", }; const Content = () => { const history = useHistory(); const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 在轮训时使用ref存值 const openNameRef = useRef(null); const location = useLocation(); console.log(location?.state?.history); const [loading, setLoading] = useState(true); const [retryLoading, setRetryLoading] = useState(false); const [data, setData] = useState({ detail: {}, rollback_state: 0, }); // 轮训的timer控制器 const timer = useRef(null); const queryRollbackProcess = () => { !timer.current && setLoading(true); fetchGet( `${apiRequest.appStore.queryRollbackProcess}/${location?.state?.history}` ) .then((res) => { handleResponse(res, (res) => { setData(res.data); if ( res.data.rollback_state == 0 || res.data.rollback_state == 1 || res.data.rollback_state == 4 ) { // 状态为未安装或者安装中 timer.current = setTimeout(() => { queryRollbackProcess(); }, 5000); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const retryRollback = () => { setRetryLoading(true); fetchPut( `${apiRequest.appStore.queryRollbackProcess}/${location?.state?.history}` ) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { queryRollbackProcess(); } }); }) .catch((e) => console.log(e)) .finally(() => { setRetryLoading(false); }); }; useEffect(() => { queryRollbackProcess(); return () => { // 页面销毁时清除延时器 clearTimeout(timer.current); }; }, []); return (
{data?.rollback_detail?.map((item, idx) => { return ( ); })}
{ let con = document.getElementById("Step4Wrapper"); return con; }} onClick={(e) => { e.preventDefault(); }} > {data?.rollback_detail?.map((item, idx) => { let hasError = item.rollback_details.filter((a) => a.rollback_state == 3) .length !== 0; return (
{item.service_name} } />
); })}
{renderStatus[data.rollback_state]} {(data.rollback_state == 0 || data.rollback_state == 1 || data.rollback_state == 4) && ( )}
{data.rollback_state == 3 && ( )}
); }; export default Content; ================================================ FILE: omp_web/src/pages/AppStore/config/Rollback/index.js ================================================ // 服务的升级和回滚 import { useHistory } from "react-router-dom"; // import { getTabKeyChangeAction } from "../../store/actionsCreators"; import styles from "./index.module.less"; import { LeftOutlined } from "@ant-design/icons"; import Content from "./content/index.js"; // 安装页面 const Rollback = () => { // const dispatch = useDispatch(); const history = useHistory(); return (
{ history.push({ pathname: "/application_management/install-record", state: { tabKey: "backoff", }, }); }} /> 服务回滚
); }; export default Rollback; ================================================ FILE: omp_web/src/pages/AppStore/config/Rollback/index.module.less ================================================ .backIcon:hover { color: rgb(46, 124, 238); } :global { .ant-anchor-ink::before { background-color: #fff!important; } } ================================================ FILE: omp_web/src/pages/AppStore/config/ScanServerModal.js ================================================ import { Button, Modal, Steps, Tooltip, Table } from "antd"; import { useEffect, useRef, useState } from "react"; import { LoadingOutlined, CheckCircleFilled, ScanOutlined, } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; const ScanServerModal = ({ scanServerModalVisibility, setScanServerModalVisibility, refresh, }) => { const [stepNum, setStepNum] = useState(0); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState(); const isOpen = useRef(null); const timer = useRef(null); // 失败时的轮训次数标识 const trainingInRotationNum = useRef(0); function fetchData(data) { // 防止在弹窗关闭后还继续轮训 if (!isOpen.current) { return; } fetchGet(apiRequest.appStore.localPackageScanResult, { params: { uuid: data.uuid, package_names: data.package_names.join(","), }, }) .then((res) => { // timer.current = setTimeout(() => { // fetchData(data); // }, 2000); if (res) handleResponse(res, (res) => { if (res.data.stage_status.includes("check")) { setStepNum(1); } if (res.data.stage_status.includes("publish")) { setStepNum(2); } setDataSource(res.data); if (res.data && res.data.stage_status.includes("ing")) { timer.current = setTimeout(() => { fetchData(data); }, 2000); } }); }) .catch((e) => { trainingInRotationNum.current++; if (trainingInRotationNum.current < 3) { setTimeout(() => { fetchData(data); }, 5000); } else { setDataSource((dataS) => { // /console.log(dataS); let arr = dataS?.package_detail?.map((item) => { return { ...item, status: 9, }; }); //console.log(arr); return { ...dataS, package_detail: arr, }; }); } }) .finally((e) => {}); } // 扫描服务端executeLocalPackageScan const executeLocalPackageScan = () => { setStepNum(0); setLoading(true); fetchPost(apiRequest.appStore.executeLocalPackageScan) .then((res) => { handleResponse(res, (res) => { if ( res.data && res.data?.package_names?.filter((item) => item).length > 0 ) { fetchData(res.data); } }); }) .catch((e) => { console.log(e); }) .finally(() => setLoading(false)); }; useEffect(() => { isOpen.current = scanServerModalVisibility; if (scanServerModalVisibility) { executeLocalPackageScan(); } }, [scanServerModalVisibility]); return ( {stepNum == 0 && "扫描服务端安装包"} {stepNum == 1 && "校验服务端安装包"} {stepNum == 2 && "发布服务端安装包"} } afterClose={() => { setDataSource([]); setStepNum(0); clearTimeout(timer.current); refresh(); }} onCancel={() => { setScanServerModalVisibility(false); }} visible={scanServerModalVisibility} footer={null} width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose > } /> } /> } /> {stepNum == 0 && (
{loading ? (

正在扫描服务端...

) : (

扫描结束,服务端暂无安装包!

请将安装包上传至{" "} omp/package_hub/back_end_verified/ {" "} 目录后重新扫描

)}
)} {stepNum == 1 && (

{dataSource?.message}

{ switch (text) { case 2: return "校验中"; break; case 1: return "校验失败"; break; case 0: return "校验成功"; break; case 9: return "网络错误"; break; default: break; } }, }, { title: "说明", key: "message", dataIndex: "message", align: "center", //width:120, ellipsis: true, render: (text) => { return (
{text ? text : "-"}
); }, }, ]} pagination={false} dataSource={dataSource?.package_detail?.map((item, idx) => { return { ...item, name: dataSource && dataSource.package_names_lst && dataSource.package_names_lst[idx], }; })} /> {dataSource?.stage_status == "check_all_failed" && (
)} )} {stepNum == 2 && (
{dataSource?.stage_status == "published" && (

发布完成

)}

{dataSource?.message}

发布完成的安装包存放路径:{" "} omp/package_hub/verified/ {" "}

{ switch (text) { case 3: return "发布成功"; break; case 4: return "发布失败"; break; case 5: return "发布中"; break; case 9: return "网络错误"; break; default: break; } }, }, { title: "说明", key: "message", dataIndex: "message", align: "center", ellipsis: true, render: (text) => { return (
{text ? text : "-"}
); }, }, ]} pagination={false} dataSource={dataSource?.package_detail?.map((item, idx) => { return { ...item, name: dataSource && dataSource.package_names_lst && dataSource.package_names_lst[idx], }; })} />
)} ); }; export default ScanServerModal; ================================================ FILE: omp_web/src/pages/AppStore/config/ServiceRollbackModal.js ================================================ import { Button, Modal, Tooltip, Input, Table } from "antd"; import { useEffect, useRef, useState } from "react"; import { SearchOutlined, SyncOutlined } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { useHistory } from "react-router-dom"; const ServiceRollbackModal = ({ sRModalVisibility, setSRModalVisibility, // dataSource, installTitle, initLoading, fixedParams, }) => { const [loading, setLoading] = useState(false); const [selectValue, setSelectValue] = useState(); const [rows, setRows] = useState([]); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); // console.log(checkedList) //应用服务选择的版本号 const versionInfo = useRef({}); const lock = useRef(false); const columns = [ { title: "名称", key: "instance_name", dataIndex: "instance_name", // align: "center", ellipsis: true, width: 150, render: (text, record) => { return (
{text}
); }, }, { title: "当前版本", key: "before_rollback_v", dataIndex: "before_rollback_v", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, { title: "回滚版本", key: "after_rollback_v", dataIndex: "after_rollback_v", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, ]; const [dataSource, setDataSource] = useState([]); const [allLength, setAllLength] = useState(0); const queryDataList = (search) => { // setRows([]); // setCheckedList([]); setLoading(true); fetchGet(`${apiRequest.appStore.canRollback}${fixedParams || ""}`, { params: { search: search, }, }) .then((res) => { handleResponse(res, (res) => { let result = formatResData(res.data.results); if (!search || search == undefined) { setAllLength(result.map((i) => i.children).flat().length); } setDataSource(result); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const formatResData = (data = []) => { // 遍历数据添加key以及判断父级数据的当前版本 // (当其子项的当前版本全部一致时设置父级数据版本为此值) let result = data.map((item) => { let currentVersion = "-"; let can_rollback = []; let c = Array.from( new Set( item.children.map((i, idx) => { item.children[idx].key = item.children[idx].instance_name; item.children[idx]._id = item.children[idx].id; item.children[idx].id = item.children[idx].instance_name; item.children[idx].isChildren = item.app_name; return i.before_rollback_v; }) ) ); let b = Array.from( new Set( item.children.map((i, idx) => { return i.after_rollback_v; }) ) ); c.length == 1 && (currentVersion = c[0]); b.length == 1 && can_rollback.push(b[0]); return { ...item, before_rollback_v: currentVersion, after_rollback_v: can_rollback, instance_name: item.app_name, id: item.app_name || item.instance_name, key: item.app_name || item.instance_name, }; }); return result; }; // 组件的checkedList并不能很好的对应业务数据需求,封装函数进行转换 const formatCheckedListData = (data) => { return data.filter((item) => { return item.isChildren; }); }; // 回滚命令下发 const doRollback = (checkedList) => { setLoading(true); fetchPost(apiRequest.appStore.doRollback, { body: { choices: checkedList.map((item) => item._id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { history.push({ pathname: "/application_management/app_store/service_rollback", state: { history: res.data.history, }, }); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { sRModalVisibility && queryDataList(); }, [sRModalVisibility]); return ( 服务回滚-选择应用服务 } width={600} afterClose={() => { setRows([]); setCheckedList([]); }} onCancel={() => { setSRModalVisibility(false); }} visible={sRModalVisibility} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
服务名称: { setSelectValue(e.target.value); if (!e.target.value) { queryDataList(); } }} onBlur={() => { queryDataList(selectValue); }} onPressEnter={() => { queryDataList(selectValue); }} suffix={ !selectValue && ( ) } />
({ // // is_continue的不能选中 // disabled: !record.is_continue, // })} rowSelection={{ onChange: (selectedRowKeys, selectedRows, select, lll) => { // 全选动作交给onchange事件 if (lock.current) { setRows(selectedRowKeys); setCheckedList(formatCheckedListData(selectedRows)); // 关闭锁,下次事件触发时优先使用onselect处理rowkey变更 lock.current = false; } }, selectedRowKeys: rows, onSelect: (record, selected, selectedRows) => { if (record.isChildren) { if (selected) { for (const item of dataSource) { if (item.instance_name == record.isChildren) { let results = item.children.filter( (i) => i.ip == record.ip ); // results是当前点击要变动的 // row中原本的数据不应该改变 // row原本数据不改变,考虑到每次都是直接选中全部ip的项,也不会出现选中已经选中项的情况 setRows((r) => { return [...r, ...results.map((k) => k.instance_name)]; }); setCheckedList((checks) => { return formatCheckedListData([...checks, ...results]); }); break; } } } else { // 删除掉和record 父级和ip一样的项 let checkedListCopy = JSON.parse( JSON.stringify(checkedList) ); let results = formatCheckedListData( checkedListCopy.filter( (i) => i.ip !== record.ip || i.isChildren !== record.isChildren ) ); setRows(results.map((i) => i.instance_name)); setCheckedList(results); } } else { // 打开锁,使用onchange事件处理rowkey变更 lock.current = true; } }, onSelectAll: (selected, selectedRows, changeRows) => { // 打开锁,使用onchange事件处理rowkey变更 lock.current = true; }, checkStrictly: false, }} />
已选择 {checkedList.length} 个
共 {allLength} 个
); }; export default ServiceRollbackModal; ================================================ FILE: omp_web/src/pages/AppStore/config/ServiceUpgradeModal.js ================================================ import { Button, Modal, Upload, message, Steps, Tooltip, Select, Switch, Input, Table, } from "antd"; import { useEffect, useRef, useState } from "react"; import { CopyOutlined, SearchOutlined, ArrowUpOutlined } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost, fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { useHistory } from "react-router-dom"; const ServiceUpgradeModal = ({ sUModalVisibility, setSUModalVisibility, // dataSource, installTitle, initLoading, }) => { const [loading, setLoading] = useState(false); const [selectValue, setSelectValue] = useState(); const [rows, setRows] = useState([]); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); // console.log(checkedList) //应用服务选择的版本号 const versionInfo = useRef({}); const lock = useRef(false); const columns = [ { title: "名称", key: "instance_name", dataIndex: "instance_name", // align: "center", ellipsis: true, width: 150, render: (text, record) => { return (
{text}
); }, }, { title: "当前版本", key: "version", dataIndex: "version", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, { title: "升级版本", key: "can_upgrade", dataIndex: "can_upgrade", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text[0].app_version || "-"; return (
{v}
); }, }, ]; const [dataSource, setDataSource] = useState([]); const [allLength, setAllLength] = useState(0) const queryDataList = (search) => { // setRows([]); // setCheckedList([]); setLoading(true); fetchGet(apiRequest.appStore.canUpgrade, { params: { search: search, }, }) .then((res) => { handleResponse(res, (res) => { let result = formatResData(res.data.results) if(!search || search == undefined){ setAllLength(result.map((i) => i.children).flat().length) } setDataSource(result); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const formatResData = (data = []) => { console.log(data); // 遍历数据添加key以及判断父级数据的当前版本 // (当其子项的当前版本全部一致时设置父级数据版本为此值) let result = data.map((item) => { let currentVersion = "-"; let can_upgrade = [item.can_upgrade]; let c = Array.from( new Set( item.children.map((i, idx) => { item.children[idx].key = item.children[idx].instance_name; item.children[idx].id = item.children[idx].instance_name; item.children[idx].isChildren = item.app_name; return i.version; }) ) ); c.length == 1 && (currentVersion = c[0]); return { ...item, version: currentVersion, can_upgrade: can_upgrade, instance_name: item.app_name, id: item.app_name || item.instance_name, key: item.app_name || item.instance_name, }; }); return result; }; // 组件的checkedList并不能很好的对应业务数据需求,封装函数进行转换 const formatCheckedListData = (data) => { return data.filter((item) => { return item.isChildren; }); }; // 升级命令下发 const doUpgrade = (checkedList) => { setLoading(true); fetchPost(apiRequest.appStore.doUpgrade, { body: { choices: checkedList.map((item) => ({ app_id: item.can_upgrade[0].app_id, service_id: item.service_id, })), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { history.push({ pathname: "/application_management/app_store/service_upgrade", state: { history: res.data.history }, }); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { sUModalVisibility && queryDataList(); }, [sUModalVisibility]); return ( 服务升级-选择应用服务 } width={600} afterClose={() => { setRows([]); setCheckedList([]); }} onCancel={() => { setSUModalVisibility(false); }} visible={sUModalVisibility} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
服务名称: { setSelectValue(e.target.value); if (!e.target.value) { queryDataList(); } }} onBlur={() => { queryDataList(selectValue); }} onPressEnter={() => { queryDataList(selectValue); }} suffix={ !selectValue && ( ) } />
({ // // is_continue的不能选中 // disabled: !record.is_continue, // })} rowSelection={{ onChange: (selectedRowKeys, selectedRows, select, lll) => { // 全选动作交给onchange事件 if (lock.current) { setRows(selectedRowKeys); setCheckedList(formatCheckedListData(selectedRows)); // 关闭锁,下次事件触发时优先使用onselect处理rowkey变更 lock.current = false; } }, selectedRowKeys: rows, onSelect: (record, selected, selectedRows) => { if (record.isChildren) { if (selected) { for (const item of dataSource) { if (item.instance_name == record.isChildren) { let results = item.children.filter( (i) => i.ip == record.ip ); // results是当前点击要变动的 // row中原本的数据不应该改变 // row原本数据不改变,考虑到每次都是直接选中全部ip的项,也不会出现选中已经选中项的情况 setRows((r) => { return [...r, ...results.map((k) => k.instance_name)]; }); setCheckedList((checks) => { return formatCheckedListData([...checks, ...results]); }); break; } } } else { // 删除掉和record 父级和ip一样的项 let checkedListCopy = JSON.parse( JSON.stringify(checkedList) ); let results = formatCheckedListData( checkedListCopy.filter( (i) => i.ip !== record.ip || i.isChildren !== record.isChildren ) ); setRows(results.map((i) => i.instance_name)); setCheckedList(results); } } else { // 打开锁,使用onchange事件处理rowkey变更 lock.current = true; } }, onSelectAll: (selected, selectedRows, changeRows) => { // 打开锁,使用onchange事件处理rowkey变更 lock.current = true; }, checkStrictly: false, }} />
已选择 {checkedList.length} 个
共 {allLength} 个
); }; export default ServiceUpgradeModal; ================================================ FILE: omp_web/src/pages/AppStore/config/Upgrade/content/component/UpgradeDetail.js ================================================ import { DownOutlined } from "@ant-design/icons"; import { useEffect, useRef, useState } from "react"; import { OmpToolTip } from "@/components"; const stepOpen = { marginTop: 10, minHeight: 30, height: 300, transition: "all .2s ease-in", //overflow: "hidden", backgroundColor: "#000", color: "#fff", padding: 10, overflowY: "auto", whiteSpace: "pre-line", }; const stepNotOpen = { height: 0, minHeight: 0, transition: "all .2s ease-in", overflow: "hidden", backgroundColor: "#f9f9f9", }; // 状态渲染规则 const renderStatus = { 0: 等待升级, 1: 正在升级, 2: 升级成功, 3: 升级失败, }; const UpgradeDetail = ({ title, ip, status, log, instance_name }) => { const containerRef = useRef(null); const [openName, setOpenName] = useState(""); useEffect(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight; }, [log]); return ( ); }; export default UpgradeDetail; ================================================ FILE: omp_web/src/pages/AppStore/config/Upgrade/content/component/UpgradeInfoItem.js ================================================ import UpgradeDetail from "./UpgradeDetail"; const UpgradeInfoItem = ({ id, data, title, log, idx }) => { return (
{title}
{data?.map((item) => { console.log(item); return ( ); })}
); }; export default UpgradeInfoItem; ================================================ FILE: omp_web/src/pages/AppStore/config/Upgrade/content/index.js ================================================ import { Button, Anchor, Spin, Progress } from "antd"; import { useSelector } from "react-redux"; import UpgradeInfoItem from "./component/UpgradeInfoItem"; import { useEffect, useRef, useState } from "react"; import { LoadingOutlined } from "@ant-design/icons"; import { apiRequest } from "@/config/requestApi"; import { fetchGet } from "@/utils/request"; import { handleResponse } from "@/utils/utils"; import { useHistory, useLocation } from "react-router-dom"; import { fetchPut } from "src/utils/request"; import ServiceRollbackModal from "../../ServiceRollbackModal"; const { Link } = Anchor; // 状态渲染规则 const renderStatus = { 0: "等待升级", 1: "正在升级", 2: "升级成功", 3: "升级失败", 4: "正在注册", }; const Content = () => { const history = useHistory(); const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 在轮训时使用ref存值 const openNameRef = useRef(null); const location = useLocation(); console.log(location?.state?.history); const [loading, setLoading] = useState(true); const [retryLoading, setRetryLoading] = useState(false); const [data, setData] = useState({ detail: {}, upgrade_state: 0, }); // 轮训的timer控制器 const timer = useRef(null); const [vfModalVisibility, setVfModalVisibility] = useState(false); const [rowId, setRowId] = useState(""); const queryUpgradeProcess = () => { !timer.current && setLoading(true); fetchGet( `${apiRequest.appStore.queryUpgradeProcess}/${location?.state?.history}` ) .then((res) => { handleResponse(res, (res) => { setData(res.data); if ( res.data.upgrade_state == 0 || res.data.upgrade_state == 1 || res.data.upgrade_state == 4 ) { // 状态为未安装或者安装中 timer.current = setTimeout(() => { queryUpgradeProcess(); }, 5000); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const retryUpgrade = () => { setRetryLoading(true); fetchPut( `${apiRequest.appStore.queryUpgradeProcess}/${location?.state?.history}` ) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { queryUpgradeProcess(); } }); }) .catch((e) => console.log(e)) .finally(() => { setRetryLoading(false); }); }; useEffect(() => { queryUpgradeProcess(); return () => { // 页面销毁时清除延时器 clearTimeout(timer.current); }; }, []); return (
{data?.upgrade_detail?.map((item, idx) => { return ( ); })}
{ let con = document.getElementById("Step4Wrapper"); return con; }} onClick={(e) => { e.preventDefault(); }} > {data?.upgrade_detail?.map((item, idx) => { let hasError = item.upgrade_details.filter((a) => a.upgrade_state == 3) .length !== 0; return (
{item.service_name} } />
); })}
{renderStatus[data.upgrade_state]} {(data.upgrade_state == 0 || data.upgrade_state == 1 || data.upgrade_state == 4) && ( )}
{data.upgrade_state == 3 && ( <> )}
); }; export default Content; ================================================ FILE: omp_web/src/pages/AppStore/config/Upgrade/index.js ================================================ // 服务的升级和回滚 import { useHistory, useLocation } from "react-router-dom"; // import { getTabKeyChangeAction } from "../../store/actionsCreators"; import { useDispatch } from "react-redux"; import { Steps } from "antd"; import { useState } from "react"; import styles from "./index.module.less"; import { LeftOutlined } from "@ant-design/icons"; import Content from "./content/index.js" // 安装页面 const Upgrade = () => { // const dispatch = useDispatch(); const history = useHistory(); return (
{ history.push({ pathname: "/application_management/install-record", state: { tabKey: "upgrade", }, }); }} /> 服务升级
); }; export default Upgrade; ================================================ FILE: omp_web/src/pages/AppStore/config/Upgrade/index.module.less ================================================ .backIcon:hover { color: rgb(46, 124, 238); } :global { .ant-anchor-ink::before { background-color: #fff!important; } } ================================================ FILE: omp_web/src/pages/AppStore/config/card.js ================================================ import { OmpToolTip } from "@/components"; import styles from "./index.module.less"; import imgObj from "./img"; import { useState } from "react"; const Card = ({ idx, history, info, tabKey, installOperation }) => { //定义命名 let nameObj = { component: { logo: "app_logo", name: "app_name", version: "app_version", description: "app_description", instance_number: "instance_number", install_url: "/application_management/app_store/component_installation", }, service: { logo: "pro_logo", name: "pro_name", version: "pro_version", description: "pro_description", instance_number: "instance_number", install_url: "/application_management/app_store/application_installation", }, }; const [isHover, setIsHover] = useState(false); return (
{ setIsHover(true); }} onMouseLeave={() => { setIsHover(false); }} >
{info[nameObj[tabKey].logo] ? (
) : (
{info[nameObj[tabKey].name] && info[nameObj[tabKey].name][0].toLocaleUpperCase()}
)}
{ history?.push({ pathname: `/application_management/app_store/app-${tabKey}-detail/${ info[nameObj[tabKey].name] }/${info[nameObj[tabKey].version]}`, }); }} >
{info[nameObj[tabKey].name]}
最新版本 {info[nameObj[tabKey].version]}

{/* */} {info[nameObj[tabKey].description]} {/* */}

已安装{info[nameObj[tabKey].instance_number]}个实例
{ history?.push({ pathname: `/application_management/app_store/app-${tabKey}-detail/${ info[nameObj[tabKey].name] }/${info[nameObj[tabKey].version]}`, }); }} > 查看
{ if (tabKey == "service") { installOperation({ product_name: info.pro_name }, "服务"); } else { installOperation({ app_name: info.app_name }, "组件"); // history?.push({ // pathname: `${nameObj[tabKey].install_url}/${ // info[nameObj[tabKey].name] // }`, // }); } }} > 安装
); }; export default Card; ================================================ FILE: omp_web/src/pages/AppStore/config/component/RenderComDependence.js ================================================ import { Form, Input, Select, Checkbox } from "antd"; import { useEffect, useState } from "react"; // 组件安装的依赖信息 const randomNumber = () => { let r = ""; let str = "QWERTYUIOPLKJHGFDSAZXCVBNM123456790"; new Array(6).fill(0).map((item) => { let num = parseInt(Math.random() * 26); r += str[num]; }); return r; }; const RenderComDependence = ({ data, form }) => { if (data.is_base_env) { return (
{data.name}
{data.version}
安装依赖
); } else { // 当is_base_env为false的渲染逻辑 return ; } }; // 分类渲染 const RenderHasData = ({ data, form }) => { if (data.cluster_info?.length == 0 && data.instance_info?.length == 0) { return ; } else { let dataSource = data.cluster_info.concat(data.instance_info); //console.log(dataSource); // 因为是两个数据源拼接并且字段不一致在这里合并并统一字段 let result = dataSource.map((item) => { if (item.ip) { return { id: `single|${JSON.stringify(item)}`, name: item.service_instance_name, }; } else { return { id: `cluster|${JSON.stringify(item)}`, name: item.cluster_name, }; } }); return ; } }; // 当判断为集群时的渲染 const RenderClusterDom = ({ data, form }) => { // 将当前数据在deployModeObj初始化 // let obj = R.clone(deployModeObj); // console.log(form.getFieldValue(`${data.name}|deploy_mode`)); return (
{data.name}
{data.version}
{/* 安装依赖 */}
); }; // 单实例没有实例名称。其他有(单独封装为了复用) const ClusterComponent = ({ data, form }) => { let deploy = data.deploy_mode && data.deploy_mode[0] ? JSON.stringify(data.deploy_mode[0]) : ""; const [deploy_mode, setDeploy_mode] = useState(deploy); useEffect(() => { form.setFieldsValue({ [`${data.name}|deploy_mode`]: JSON.stringify(deploy), [`${data.name}|modeName`]: `${data.name}-${randomNumber()}`, }); }, []); return ( <>
集群模式
} name={`${data.name}|deploy_mode`} >
{deploy_mode !== '{"key":"single","name":"单实例"}' ? (
集群名称
} name={`${data.name}|modeName`} rules={[{ required: true, message: "请输入集群名称" }]} >
) : (
)} ); }; // 判断为实例的渲染 const RenderInstanceDom = ({ instanceData, data, form }) => { const [isMultiplexing, setIsMultiplexing] = useState("checked"); useEffect(() => { form.setFieldsValue({ [`${data.name}|isMultiplexing`]: "checked", [`${data.name}|instance`]: instanceData[0].id, }); }, []); return (
{data.name}
{data.version}
{isMultiplexing == "checked" ? ( <>
选择实例
} name={`${data.name}|instance`} >
) : ( )}
存在实例,是否复用?
{ e.target.checked ? setIsMultiplexing("checked") : setIsMultiplexing("unChecked"); }} style={{ marginRight: 10 }} > 复用
); }; export default RenderComDependence; ================================================ FILE: omp_web/src/pages/AppStore/config/detail.js ================================================ import img from "@/config/logo/logo.svg"; import styles from "./index.module.less"; import { LeftOutlined } from "@ant-design/icons"; import { Button, message, Select, Spin, Table } from "antd"; import { useEffect, useState } from "react"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { useHistory, useLocation } from "react-router-dom"; import { handleResponse } from "@/utils/utils"; import imgObj from "./img"; import moment from "moment"; import { getTabKeyChangeAction } from "../store/actionsCreators"; import { useDispatch } from "react-redux"; import { getStep1ChangeAction } from "./Installation/store/actionsCreators"; import { getUniqueKeyChangeAction } from "../store/actionsCreators"; const AppStoreDetail = () => { const dispatch = useDispatch(); const history = useHistory(); const location = useLocation(); let arr = location.pathname.split("/"); let name = arr[arr.length - 2]; let verson = arr[arr.length - 1]; // true 是组件, false是服务 let keyTab = location.pathname.includes("component"); //定义命名 let nameObj = keyTab ? { logo: "app_logo", name: "app_name", version: "app_version", description: "app_description", instance_number: "instance_number", package_md5: "app_package_md5", type: "app_labels", user: "app_operation_user", dependence: "app_dependence", instances_info: "app_instances_info", install_url: "/application_management/app_store/component_installation", } : { logo: "pro_logo", name: "pro_name", version: "pro_version", description: "pro_description", instance_number: "instance_number", package_md5: "pro_package_md5", type: "pro_labels", user: "pro_operation_user", dependence: "pro_dependence", pro_services: "pro_services", instances_info: "pro_instances_info", install_url: "/application_management/app_store/application_installation", }; const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState({}); const [versionValue, setVersionValue] = useState(""); // 安装操作的loading const [installLoading, setInstallLoading] = useState(false); // 定义全部实例信息 const [allInstancesInfo, setAllInstancesInfo] = useState([]); // 是否查看全部版本 const [isAll, setIsAll] = useState(false); function fetchData() { setLoading(true); fetchGet( keyTab ? apiRequest.appStore.ProductDetail : apiRequest.appStore.ApplicationDetail, { params: { [keyTab ? "app_name" : "pro_name"]: name, }, } ) .then((res) => { handleResponse(res, (res) => { setAllInstancesInfo(() => { return res.data.versions .map((item) => { return item[nameObj.instances_info]; }) .flat(); }); setVersionValue(verson); let y = (res.data.versions = res.data.versions.map((item) => { // arr 为全部数据中version重复数据 let arr = []; res.data.versions .filter((i) => i[nameObj.version] == item[nameObj.version]) .map((v) => { arr = [...arr, ...v[nameObj.instances_info]]; }); return { ...item, [nameObj.instances_info]: arr, }; })); setDataSource(() => { let obj = {}; res.data.versions.map((item) => { obj[item[nameObj.version]] = item; }); return { ...res.data, versionObj: obj, }; }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } let currentVersionDataSource = dataSource.versionObj ? dataSource.versionObj[versionValue] : {}; const install = () => { setInstallLoading(true); if (keyTab) { fetchPost(apiRequest.appStore.createComponentInstallInfo, { body: { high_availability: true, install_component: [ { name: dataSource[nameObj.name], version: versionValue }, ], }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { dispatch(getStep1ChangeAction(res.data.data)); dispatch(getUniqueKeyChangeAction(res.data.unique_key)); } history.push("/application_management/app_store/installation"); }); }) .catch((e) => console.log(e)) .finally(() => { setInstallLoading(false); }); } else { fetchGet(apiRequest.appStore.queryBatchInstallationServiceList, { params: { product_name: dataSource[nameObj.name], }, }) .then((res) => { handleResponse(res, (res) => { if (res.data && res.data.data) { if (res.data.data.length == 1 && res.data.data[0].is_continue) { dispatch(getUniqueKeyChangeAction(res.data.unique_key)); fetchPost(apiRequest.appStore.createInstallInfo, { body: { high_availability: true, install_product: [ { name: dataSource[nameObj.name], version: versionValue, }, ], unique_key: res.data.unique_key, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { dispatch(getStep1ChangeAction(res.data.data)); } history.push( "/application_management/app_store/installation" ); }); }) .catch((e) => console.log(e)) .finally(() => { setInstallLoading(false); }); } else { message.warning("该应用已经存在,不可重复安装"); setInstallLoading(false); } // console.log(res.data.data); // setBIserviceList(res.data.data); } }); }) .catch((e) => console.log(e)) .finally(() => {}); // console.log("服务"); } }; let tableData = isAll ? allInstancesInfo : currentVersionDataSource[nameObj.instances_info]; useEffect(() => { fetchData(); }, []); return (
{ keyTab ? dispatch(getTabKeyChangeAction("component")) : dispatch(getTabKeyChangeAction("service")); history?.push({ pathname: `/application_management/app_store`, }); }} />{" "} {dataSource[nameObj.name]}
{/* */} 版本:{" "}
{currentVersionDataSource[nameObj.logo] ? (
) : (
{dataSource[nameObj.name] && dataSource[nameObj.name][0].toLocaleUpperCase()}
)}
{currentVersionDataSource[nameObj.description]}
类别:
{currentVersionDataSource[nameObj.type]?.join(",")}
发布时间:
{moment(currentVersionDataSource?.created).format( "YYYY-MM-DD HH:mm:ss" )}
MD5:
{currentVersionDataSource[nameObj.package_md5]}
发布人:
{currentVersionDataSource[nameObj.user]}
依赖信息
{currentVersionDataSource[nameObj.dependence] ? (
) : (

)} {!keyTab && (
包含服务
{currentVersionDataSource.pro_services ? (
{ return text || "-"; }, }, { title: "MD5", key: "md5", dataIndex: "md5", align: "center", render: (text) => { return text || "-"; }, }, { title: "发布时间", key: "created", dataIndex: "created", align: "center", render: (text) => { return text || "-"; }, }, { title: "安装", key: "c", dataIndex: "c", align: "center", render: (text, record) => { return ( { setLoading(true); fetchPost( apiRequest.appStore.createComponentInstallInfo, { body: { high_availability: false, install_component: [{ name:record.name, version:record.version }], }, } ) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { dispatch( getStep1ChangeAction(res.data.data) ); dispatch( getUniqueKeyChangeAction( res.data.unique_key ) ); } history.push( "/application_management/app_store/installation" ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }} > 点击安装 ); }, }, ]} pagination={false} dataSource={currentVersionDataSource.pro_services} /> ) : (

)} )} {keyTab ? (
实例信息 {isAll ? ( { setIsAll(false); }} > 查看当前版本 ) : ( { setIsAll(true); }} > 查看全部版本 )}
{tableData && tableData.length == 0 ? (

) : (
{ if (!text) { return "-"; } return text.map((i) => i.default).join(", "); }, }, { title: "版本", key: "app_version", dataIndex: "app_version", align: "center", }, { title: "模式", key: "mode", dataIndex: "mode", align: "center", }, { title: "安装时间", key: "created", dataIndex: "created", align: "center", render: (text) => { return moment(text).format("YYYY-MM-DD HH:mm:ss"); }, }, ]} //pagination={false} dataSource={tableData} /> )} ) : (
实例信息 {isAll ? ( { setIsAll(false); }} > 查看当前版本 ) : ( { setIsAll(true); }} > 查看全部版本 )}
{tableData && tableData.length == 0 ? (

) : (
{ if (!text) { return "-"; } return text.map((i) => i.default).join(","); }, }, { title: "安装时间", key: "created", dataIndex: "created", align: "center", render: (text) => { return moment(text).format("YYYY-MM-DD HH:mm:ss"); }, }, ]} //pagination={false} dataSource={tableData} /> )} )} ); }; export default AppStoreDetail; ================================================ FILE: omp_web/src/pages/AppStore/config/img.js ================================================ const componentImgStr = `` const serviceImgStr = `` export default { component:componentImgStr, service:serviceImgStr } ================================================ FILE: omp_web/src/pages/AppStore/config/index.module.less ================================================ .cardContainer:hover { //top: -2px !important; box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3); color: black !important; } .cardContainer { cursor: pointer; .cardContent { height: 76%; display: flex; padding-top: 10px; .text { font-size: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; //height: r(210); //font-size: r(42); // white-space: nowrap; text-overflow: ellipsis; overflow: hidden; width: 100%; margin: 0; } } .cardBtn { display: flex; //justify-content: space-around; border-top: solid 1px #e7e7e7; align-items: center; height: 24%; // /color: #818181; font-size: 13px; & > div { width: 50%; text-align: center; } // & > div:hover { // color: rgb(46, 124, 238); // } } } .detailContainer { background-color: #fff; padding-top: 10px; padding-left: 3px; padding-right: 3px; .detailHeader { overflow: hidden; background-color: #f5f6f5; height: 40px; line-height: 40px; padding-left: 20px; display: flex; justify-content: space-between; } .detailTitle { height: 120px; display: flex; padding-left: 20px; padding-top: 0px; padding-right: 200px; // background-color: aliceblue; align-items: center; .detailTitleDescribe { height: 80px; padding-left: 60px; //word-wrap: break-word; width: calc(100% - 120px); color: #4f4f4f; position: relative; .detailTitleDescribeText { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; //height: r(210); //font-size: r(42); // white-space: nowrap; text-overflow: ellipsis; overflow: hidden; // /width: 100%; } } } .detailContent { color: #4f4f4f; padding-left: 30px; .detailContentItem { display: flex; padding-top: 15px; .detailContentItemLabel { width: 120px; } } } .detailDependence { padding-left: 30px; margin-top: 40px; font-size: 16px; //background-color: red; .detailDependenceTable { //width: 100%; margin-right: 20px; border: 1px solid #d6d6d6; margin-top: 20px; } } } .backIcon:hover { color: rgb(46, 124, 238); } ================================================ FILE: omp_web/src/pages/AppStore/index.js ================================================ import { Input, Button, Pagination, Empty, Spin, Dropdown, Menu } from "antd"; import { useEffect, useRef, useState } from "react"; import styles from "./index.module.less"; import { SearchOutlined, DownloadOutlined, DownOutlined, SendOutlined, ScanOutlined, ArrowUpOutlined, SyncOutlined, DeleteOutlined, ZoomInOutlined, } from "@ant-design/icons"; import Card from "./config/card.js"; import { useSelector, useDispatch } from "react-redux"; import { useHistory } from "react-router-dom"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse, downloadFile } from "@/utils/utils"; import ReleaseModal from "./config/ReleaseModal.js"; import ScanServerModal from "./config/ScanServerModal"; import DeleteServerModal from "./config/DeleteServerModal"; // 批量安装弹框组件 import BatchInstallationModal from "./config/BatchInstallationModal"; import ServiceUpgradeModal from "./config/ServiceUpgradeModal"; import ServiceRollbackModal from "./config/ServiceRollbackModal"; import { getTabKeyChangeAction, getUniqueKeyChangeAction, } from "./store/actionsCreators"; import GetServiceModal from "./config/GetServiceModal"; const AppStore = () => { // appStoreTabKey const appStoreTabKey = useSelector((state) => state.appStore.appStoreTabKey); const dispatch = useDispatch(); // 视口高度 const viewHeight = useSelector((state) => state.layouts.viewSize.height); const history = useHistory(); const [tabKey, setTabKey] = useState(appStoreTabKey); const [searchKey, setSearchKey] = useState("全部"); const [searchData, setSearchData] = useState([]); const [searchName, setSearchName] = useState(""); const [total, setTotal] = useState(0); const [timeUnix, setTimeUnix] = useState(""); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: viewHeight > 955 ? 12 : 8, total: 0, searchParams: {}, }); // 发布操作 const [releaseModalVisibility, setReleaseModalVisibility] = useState(false); // 扫描服务端 const [scanServerModalVisibility, setScanServerModalVisibility] = useState(false); // 服务升级操作弹框 const [sUModalVisibility, setSUModalVisibility] = useState(false); // 服务回退操作弹框 const [sRModalVisibility, setSRModalVisibility] = useState(false); // 批量安装弹框 const [bIModalVisibility, setBIModalVisibility] = useState(false); // 删除应用商店 const [deleteServerVisibility, setDeleteServerVisibility] = useState(false); // 批量安装的应用服务列表 const [bIserviceList, setBIserviceList] = useState([]); // 服务纳管弹框 const [serviceGetModalVisibility, setServiceGetModalVisibility] = useState(false); const [serviceGetData, setServiceGetData] = useState([]); const [initData, setInitData] = useState([]); // 批量安装标题文案 const installTitle = useRef("批量"); function fetchData(pageParams = { current: 1, pageSize: 8 }, searchParams) { setLoading(true); fetchGet( searchParams.tabKey == "component" ? apiRequest.appStore.queryComponents : apiRequest.appStore.queryServices, { params: { page: pageParams.current, size: pageParams.pageSize, ...searchParams, tabKey: null, }, } ) .then((res) => { handleResponse(res, (res) => { // 获得真正的总数,要查询条件都为空时 let obj = { ...searchParams }; delete obj.tabKey; let arr = Object.values(obj).filter((i) => i); if (arr.length == 0) { setTotal(res.data.count); } setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchSearchlist(); //fetchIPlist(); }); } // 获取批量安装应用服务列表 const queryBatchInstallationServiceList = (queryData) => { setLoading(true); fetchGet(apiRequest.appStore.queryBatchInstallationServiceList, { params: queryData, }) .then((res) => { handleResponse(res, (res) => { if (res.data && res.data.data) { setBIserviceList( res.data.data.map((item) => ({ ...item, id: item.name })) ); dispatch(getUniqueKeyChangeAction(res.data.unique_key)); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 获取安装基础组件列表 const queryInstallComponent = (queryData) => { setLoading(true); fetchGet(apiRequest.appStore.ProductDetail, { params: queryData, }) .then((res) => { handleResponse(res, (res) => { console.log(res); if (res.data) { let serverlist = {}; serverlist.name = res.data.app_name; serverlist.is_continue = true; serverlist.version = res.data.versions.map((item) => { return item.app_version; }); setBIserviceList([serverlist]); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const fetchSearchlist = () => { //setSearchLoading(true); fetchGet(apiRequest.appStore.queryLabels, { params: { label_type: tabKey == "component" ? 0 : 1, }, }) .then((res) => { handleResponse(res, (res) => { setSearchData(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { //setSearchLoading(false); }); }; // 获取可纳管服务列表 const queryAllAppList = () => { setLoading(true); fetchGet(apiRequest.appStore.queryAppList) .then((res) => { handleResponse(res, (res) => { const resArr = res.data; for (let i = 0; i < resArr.length; i++) { const element = resArr[i]; if (element.hasOwnProperty("child")) { element.children = element.child[element.version[0]]; } } setServiceGetData(resArr); setInitData(resArr); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, tabKey: tabKey, type: searchKey == "全部" ? null : searchKey, } ); return () => { dispatch(getTabKeyChangeAction(tabKey)); }; }, [tabKey, searchKey]); const refresh = () => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, tabKey: tabKey, type: searchKey == "全部" ? null : searchKey, } ); }; return (
{ setPagination({ current: 1, pageSize: viewHeight > 955 ? 12 : 8, total: 0, searchParams: {}, }); setSearchName(""); setSearchKey("全部"); if (e.target.innerHTML == "应用服务") { setTabKey("service"); } else if (e.target.innerHTML == "基础组件") { setTabKey("component"); } }} >
基础组件
|
应用服务
} style={{ marginRight: 10, width: 200 }} value={searchName} allowClear onChange={(e) => { setSearchName(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, [tabKey == "component" ? "app_name" : "pro_name"]: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, [tabKey == "component" ? "app_name" : "pro_name"]: searchName, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, [tabKey == "component" ? "app_name" : "pro_name"]: searchName, } ); }} /> { setTimeUnix(new Date().getTime()); setReleaseModalVisibility(true); }} >
上传发布服务
{ setScanServerModalVisibility(true); }} >
扫描发布服务
setDeleteServerVisibility(true)} >
删除
{ setSUModalVisibility(true); }} >
服务升级
{ setSRModalVisibility(true); }} >
服务回滚
{ queryAllAppList(); setServiceGetModalVisibility(true); }} >
服务纳管
} placement="bottomRight" >

{ // 在把含有&符号的字符串存进数据库后,再读出来的时候,发现&都变成了& let str = e.target.innerHTML.replace( new RegExp("&", "g"), "&" ); if (searchData?.indexOf(str) !== -1 || str == "全部") { setSearchKey(str); } }} >

全部

{searchData.map((item) => { return (

{item}

); })}
共收录 {total} 个{tabKey == "component" ? "基础组件" : "应用服务"}
{dataSource.length == 0 ? ( 955 ? 500 : 300, flexDirection: "column", }} description={ tabKey == "component" ? "商店暂无基础组件" : "商店暂无应用服务" } /> ) : ( <> {dataSource.map((item, idx) => { return ( { if (type == "服务") { installTitle.current = type; queryBatchInstallationServiceList(queryData); } else { installTitle.current = type; // 组件安装组件列表 queryInstallComponent(queryData); } setBIModalVisibility(true); }} /> ); })} )}
{dataSource.length !== 0 && (
{ fetchData( { ...pagination, current: e }, { ...pagination.searchParams, } ); }} current={pagination.current} pageSize={pagination.pageSize} total={pagination.total} />
)}
); }; export default AppStore; ================================================ FILE: omp_web/src/pages/AppStore/index.module.less ================================================ .header { background-color: #fff; padding-bottom: 10px; //display: flex; //padding:20px; .headerTabRow { display: flex; //align-items: center; justify-content: space-between; .headerTab { display: flex; color: #4f4f4f; font-size: 14px; margin-top: 20px; & > div:nth-child(1) { padding-left: 20px; //margin-top: 20px; cursor: pointer; } & > div:nth-child(2) { padding-left: 10px; padding-right: 10px; //margin-top: 20px; color: #b9b9b9; } & > div:nth-child(3) { // margin-top: 20px; cursor: pointer; } } .headerBtn { display: flex; padding-top: 10px; padding-bottom: 5px; position: relative; top: 3px; padding-right:20px } } .headerHr { border-top: solid 1px #e7e7e7; border-left: solid 1px #e7e7e7; } .headerSearch { padding-left: 20px; display: flex; justify-content: space-between; font-size: 12px; .headerSearchCondition { position: relative; top:5px; display: flex; & > p { padding-right: 20px; cursor: pointer; } } .headerSearchInfo { color: #818181; padding-right: 10px; } } } ================================================ FILE: omp_web/src/pages/AppStore/store/actionsCreators.js ================================================ import * as actionTypes from "./constants"; export const getTabKeyChangeAction = (value) => ({ type: actionTypes.CHANGE_APPSTORETABKEY, payload: { appStoreTabKey: value, }, }); export const getUniqueKeyChangeAction = (value) => ({ type: actionTypes.SETUNIQUE_KEY, payload: { uniqueKey: value, }, }); ================================================ FILE: omp_web/src/pages/AppStore/store/constants.js ================================================ export const CHANGE_APPSTORETABKEY = "CHANGE_APPSTORETABKEY"; export const SETUNIQUE_KEY = "SETUNIQUE_KEY"; ================================================ FILE: omp_web/src/pages/AppStore/store/index.js ================================================ import reducer from "./reduer"; export { reducer }; ================================================ FILE: omp_web/src/pages/AppStore/store/reduer.js ================================================ import * as actionTypes from "./constants"; const defaultState = { appStoreTabKey: "component", uniqueKey: "", }; function reducer(state = defaultState, action) { switch (action.type) { case actionTypes.CHANGE_APPSTORETABKEY: return { ...state, appStoreTabKey: action.payload.appStoreTabKey }; case actionTypes.SETUNIQUE_KEY: return { ...state, uniqueKey: action.payload.uniqueKey }; default: return state; } } export default reducer; ================================================ FILE: omp_web/src/pages/BackupRecords/config/columns.js ================================================ import { renderDisc } from "@/utils/utils"; import { Tooltip } from "antd"; import moment from "moment"; const renderResult = (text) => { switch (text) { case 0: return {renderDisc("critical", 7, -1)}失败; case 1: return {renderDisc("normal", 7, -1)}成功; case 2: return {renderDisc("warning", 7, -1)}执行中; } }; const getColumnsConfig = (setRow, setDeleteOneModal) => { // 推送邮件相关数据 return [ { title: "任务名称", key: "backup_name", dataIndex: "backup_name", align: "center", width: 240, ellipsis: true, fixed: "left", render: (text) => { return ( {text || "-"} ); }, }, { title: "状态", key: "result", dataIndex: "result", align: "center", width: 100, render: (text) => { return renderResult(text); }, }, { title: "备份实例", key: "content", dataIndex: "content", align: "center", width: 140, ellipsis: true, }, { title: "备份文件", key: "file_name", dataIndex: "file_name", align: "center", width: 180, ellipsis: true, render: (text, record) => { if (record?.file_deleted) { return "-"; } return ( {text || "-"} ); }, }, { title: "文件大小", key: "file_size", dataIndex: "file_size", align: "center", width: 80, ellipsis: true, // render: (text) => { // return {text ? `${text} M` : "-"}; // }, }, { title: "备份路径", key: "retain_path", dataIndex: "retain_path", width: 160, align: "center", ellipsis: true, render: (text) => { return ( {text || "-"} ); }, }, { title: "远程路径", key: "remote_path", dataIndex: "remote_path", width: 160, align: "center", ellipsis: true, render: (text) => { return ( {text || "-"} ); }, }, { title: "过期时间", key: "expire_time", dataIndex: "expire_time", width: 180, align: "center", ellipsis: true, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } return "-"; }, }, { title: "信息", key: "message", dataIndex: "message", align: "center", width: 180, ellipsis: true, render: (text) => { return ( {text || "-"} ); }, }, { title: "操作", width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: (text, record, index) => { return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/BackupRecords/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal } from "@/components"; import { Button, message } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import getColumnsConfig from "./config/columns"; import { ExclamationCircleOutlined, } from "@ant-design/icons"; import { useHistory, useLocation } from "react-router-dom"; const BackupRecords = () => { const location = useLocation(); const history = useHistory(); const [loading, setLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false); //选中的数据 const [checkedList, setCheckedList] = useState([]); //table表格数据 const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 定义row存数据 const [row, setRow] = useState({}); // 删除文件 const [deleteModal, setDeleteModal] = useState(false); const [deleteOneModal, setDeleteOneModal] = useState(false); // 列表查询 function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.dataBackup.queryBackupHistory, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); }); } // 删除 const deleteBackup = (deleteType = null) => { setDeleteLoading(true); fetchPost(apiRequest.dataBackup.queryBackupHistory, { body: { ids: deleteType === "only" ? [row.id] : checkedList.map((e) => e.id), }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("删除文件成功"); setCheckedList([]); setDeleteModal(false); setDeleteOneModal(false); fetchData({ current: pagination.current, pageSize: pagination.pageSize, }); } } }) .catch((e) => console.log(e)) .finally(() => setDeleteLoading(false)); }; useEffect(() => { fetchData({ current: pagination.current, pageSize: pagination.pageSize }); }, []); return (
{/* */}
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig(setRow, setDeleteOneModal)} notSelectable={(record) => ({ // 执行中不能选中 disabled: record.result === 2, })} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
提示 } loading={deleteLoading} onFinish={() => { deleteBackup(); }} >
确定{" "} 删除 {checkedList.length} 条{" "} 备份记录 吗?
提示 } loading={deleteLoading} onFinish={() => { deleteBackup("only"); }} >
确定 删除 当前{" "} 备份记录 吗?
); }; export default BackupRecords; ================================================ FILE: omp_web/src/pages/BackupRecords/index.module.less ================================================ .machineManagement { display: flex; } .subMenu { width: 160px; } .warningSearch { display: flex; margin-top: 10px; margin-bottom: 10px; & > div:nth-child(1) { margin-right: 10px; } & > div:last-child { margin-left: auto; } } .antdTableExpandedRow { margin: 0; padding: 0; height: 20px; display: flex; span { margin-left: 60px; } } .redType { background-color: #ff4d4f; color: #fff; border-color:#ff4d4f; } .formItem { margin-bottom: 15px; display: flex; justify-content: center; align-items: center; & > span:nth-child(1) { display: inline-block; width: 100px; font-size: 14px; //font-weight: 500; color: #333; text-align: right; } & > input:nth-child(2) { width: 240px; } } .machineTable { //cursor: url('../../public/conf/logo.svg'),default; cursor: pointer; } .omp_spin_wrapper{ height: calc(100%); } :global { .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover { background-color:#e6f1f6; color:#2e7cee } //悬停样式会覆盖disable样式,在这里把disable权限提高 .ant-dropdown-menu-item-disabled { background-color: #fff!important; color:rgba(0, 0, 0, 0.25)!important } // .ant-drawer .ant-drawer-content { // height: calc(100% - 80px); // } } ================================================ FILE: omp_web/src/pages/BackupStrategy/CustomModal.js ================================================ import { Button, Modal, Input, Table, Tooltip, Form, Spin } from "antd"; import { useEffect, useState } from "react"; import { CopyOutlined, SearchOutlined, PlusSquareOutlined, FormOutlined, } from "@ant-design/icons"; export const AddCustomModal = ({ customModalType, addCustom, loading, modalForm, addModalVisibility, setAddModalVisibility, updateCustomInfo, setUpdateCustomData, }) => { return ( { setAddModalVisibility(false); setUpdateCustomData({}); modalForm.setFieldsValue({ field_k: "", field_v: "", notes: "" }); }} visible={addModalVisibility} title={ {customModalType === "add" ? ( ) : ( )} {customModalType === "add" ? "添加自定义参数" : "编辑自定义参数"} } zIndex={1004} footer={null} destroyOnClose >
{ if (customModalType === "add") { addCustom(data); } else { updateCustomInfo(data); } }} form={modalForm} initialValues={{ field_k: "", field_v: "", notes: "", }} >
); }; export const CustomModal = ({ modalVisibility, setModalVisibility, modalLoading, customData, setCustomData, initData, setCustomModalType, setAddModalVisibility, deleteCustomInfo, modalForm, setRow, }) => { const [searchName, setSearchName] = useState(""); const columns = [ { title: "序号", key: "_idx", dataIndex: "_idx", align: "center", ellipsis: true, width: 40, render: (text, record) => { return text; }, }, { title: "名称", key: "field_k", dataIndex: "field_k", align: "center", ellipsis: true, width: 80, render: (text, record) => { return ( {text ? text : "-"} ); }, }, { title: "值", key: "field_v", dataIndex: "field_v", align: "center", ellipsis: true, width: 150, }, { title: "备注", key: "notes", dataIndex: "notes", align: "center", ellipsis: true, width: 100, render: (text, record) => { return ( {text ? text : "-"} ); }, }, { title: "操作", width: 60, key: "", dataIndex: "", align: "center", render: (text, record) => { return ( ); }, }, ]; useEffect(() => {}, []); return ( 自定义参数 } width={860} onCancel={() => { setModalVisibility(false); }} visible={modalVisibility} footer={null} //width={1000} bodyStyle={{ paddingLeft: 30, paddingRight: 30, marginTop: -10, }} destroyOnClose zIndex={1002} >
} style={{ width: 220, }} value={searchName} onChange={(e) => { setSearchName(e.target.value); if (e.target.value === "") { setCustomData(initData); } }} onPressEnter={() => { setCustomData( initData.filter((i) => i.field_k.includes(searchName)) ); }} />
{ return record.id; }} pagination={false} />
); }; ================================================ FILE: omp_web/src/pages/BackupStrategy/StrategyModal.js ================================================ import { Button, Modal, Input, Tooltip, Form, TimePicker, Spin, Switch, Select, } from "antd"; import { PlusSquareOutlined, FormOutlined, InfoCircleOutlined, } from "@ant-design/icons"; export const AddStrategyModal = ({ strategyModalType, addStrategy, updateStrategy, loading, modalForm, addModalVisibility, setAddModalVisibility, canBackupIns, initData, strategyFormInit, keyArr, setKeyArr, weekData, frequency, setFrequency, }) => { return ( { setAddModalVisibility(false); setKeyArr([]); modalForm.setFieldsValue(strategyFormInit); }} visible={addModalVisibility} title={ {strategyModalType === "add" ? ( ) : ( )} {strategyModalType === "add" ? "添加备份策略" : "编辑备份策略"} } zIndex={1004} footer={null} destroyOnClose >
{ if (strategyModalType === "add") { addStrategy(data); } else { updateStrategy(data); } }} form={modalForm} initialValues={strategyFormInit} > { if (value) { if (value.match(/^[ ]*$/)) { return Promise.reject("请输入备份路径"); } return Promise.resolve("success"); } else { return Promise.resolve("success"); } }, }, ]} > {frequency == "week" && ( )} {frequency == "month" && ( )}
); }; ================================================ FILE: omp_web/src/pages/BackupStrategy/config/columns.js ================================================ import { renderDisc } from "@/utils/utils"; import { Tooltip } from "antd"; import moment from "moment"; const getColumnsConfig = ( setStrategyRow, setDeleteStrategyModal, setStrategyModalType, setStrategyModalVisibility, setExecuteVisible, strategyForm, queryCustom, setKeyArr, weekData, setFrequency ) => { return [ { title: "序号", key: "_idx", dataIndex: "_idx", align: "center", width: 40, fixed: "left", }, { title: "备份实例", key: "backup_instances", dataIndex: "backup_instances", align: "center", width: 200, ellipsis: true, render: (text) => { return ( {text.join(",")} ); }, }, { title: "是否生效", key: "is_on", dataIndex: "is_on", align: "center", width: 60, ellipsis: true, render: (text) => { if (text) { return {renderDisc("normal", 7, -1)}是; } else { return {renderDisc("critical", 7, -1)}否; } }, }, { title: "定时策略", key: "crontab_detail", dataIndex: "crontab_detail", align: "center", width: 150, ellipsis: true, render: (text) => { if (text.day_of_month !== "*") { return ( 每月 {text.day_of_month} 日 {text.hour}:{text.minute} ); } else if (text.day_of_week !== "*") { return ( 每周 {weekData[text.day_of_week]} {text.hour}:{text.minute} ); } else { return ( 每天 {text.hour}:{text.minute} ); } }, }, { title: "保留路径", key: "retain_path", dataIndex: "retain_path", align: "center", width: 150, ellipsis: true, render: (text) => { return ( {text || "-"} ); }, }, { title: "保留时间", key: "retain_day", dataIndex: "retain_day", align: "center", width: 100, ellipsis: true, render: (text) => { if (text === -1) return 永久保留; return {text} 天; }, }, { title: "操作", width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: (text, record, index) => { return (
setStrategyRow(record)} > setExecuteVisible(true)}>执行 { queryCustom(); setStrategyModalType("update"); setStrategyModalVisibility(true); const customInfo = record.backup_custom.map((i) => { return { key: i.id, value: i.id, label: [ [{i.field_k}] , i.field_v, ], }; }); setKeyArr( customInfo.map((i) => { return i.label[0].props.children[1]; }) ); const frType = record.crontab_detail.day_of_month !== "*" ? "month" : record.crontab_detail.day_of_week !== "*" ? "week" : "day"; const stRes = { frequency: frType, time: moment( `${record.crontab_detail.hour}:${record.crontab_detail.minute}`, "HH:mm" ), }; setFrequency(frType); if (frType === "month") stRes["month"] = record.crontab_detail.day_of_month; if (frType === "week") stRes["week"] = record.crontab_detail.day_of_week; strategyForm.setFieldsValue({ backup_instances: record.backup_instances, backup_custom: customInfo, retain_path: record.retain_path, retain_day: record.retain_day, is_on: record.is_on, strategy: stRes, }); }} > 编辑 setDeleteStrategyModal(true)} > 删除
); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/BackupStrategy/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal } from "@/components"; import { Form, Button, message } from "antd"; import { useState, useEffect } from "react"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchDelete, fetchPost, fetchPut } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import styles from "./index.module.less"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import moment from "moment"; import getColumnsConfig from "./config/columns"; import { AddCustomModal, CustomModal } from "./CustomModal.js"; import { AddStrategyModal } from "./StrategyModal"; import { useHistory, useLocation } from "react-router-dom"; const BackupStrategy = () => { const location = useLocation(); const history = useHistory(); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); // 自定义参数 const [customModalVisibility, setCustomModalVisibility] = useState(false); const [customLoading, setCustomLoading] = useState(false); const [customData, setCustomData] = useState(false); const [initData, setinitData] = useState([]); // 增/改自定义参数共用 const [customModalType, setCustomModalType] = useState("add"); const [addModalVisibility, setAddModalVisibility] = useState(false); const [addLoading, setAddLoading] = useState(false); // 自定义参数表单数据 const [row, setRow] = useState({}); const [customModalForm] = Form.useForm(); const [strategyRow, setStrategyRow] = useState({}); const [updateCustomVisibility, setUpdateCustomVisibility] = useState(false); const [updateCustomData, setUpdateCustomData] = useState({}); const [updateRepeatName, setUpdateRepeatName] = useState(""); const [deleteCustomVisibility, setDeleteCustomVisibility] = useState(false); const [deleteRepeatName, setDeleteRepeatName] = useState(""); // 增/改备份策略共用 const [strategyModalType, setStrategyModalType] = useState("add"); const [strategyModalVisibility, setStrategyModalVisibility] = useState(false); const [strategyLoading, setStrategyLoading] = useState(false); const [keyArr, setKeyArr] = useState([]); // 备份策略表单 const [strategyForm] = Form.useForm(); // 备份组件全量数据 const [canBackupIns, setCanBackupIns] = useState([]); // 删除策略 const [deleteStrategyModal, setDeleteStrategyModal] = useState(false); // 执行策略 const [executeVisible, setExecuteVisible] = useState(false); const [frequency, setFrequency] = useState("day"); // 星期汉字映射 let weekData = [ "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日", ]; // 策略表单初始值 const strategyFormInit = { retain_path: "/data/omp/data/backup/", retain_day: 7, is_on: false, strategy: { frequency: "day", time: moment("00:00", "HH:mm"), week: "0", month: "1", }, backup_instances: [], backup_custom: [], }; // 数据备份策略列表查询 const fetchData = () => { setLoading(true); fetchGet(apiRequest.dataBackup.strategySetting) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.map((item, idx) => { return { ...item, _idx: idx + 1, }; }) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 自定义参数查询 const queryCustom = () => { setCustomLoading(true); fetchGet(apiRequest.dataBackup.backupCustom) .then((res) => { handleResponse(res, (res) => { const resData = res.data.map((item, idx) => { return { ...item, _idx: idx + 1, }; }); setCustomData(resData); setinitData(resData); }); }) .catch((e) => console.log(e)) .finally(() => { setCustomLoading(false); }); }; // 添加自定义参数 const addCustom = (data) => { setAddLoading(true); fetchPost(apiRequest.dataBackup.backupCustom, { body: data, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("添加自定义参数成功"); customModalForm.setFieldsValue({ field_k: "", field_v: "", notes: "", }); queryCustom(); setAddModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setAddLoading(false); }); }; // 修改自定义参数 - 提示存在实例使用 const updateCustomInfo = (data) => { setAddLoading(true); fetchGet(apiRequest.dataBackup.backupRepeatCustom, { params: { id: row.id, }, }) .then((res) => { handleResponse(res, (res) => { const repeatName = res.data[0].name; if (repeatName) { setUpdateRepeatName(repeatName); setUpdateCustomData(data); setUpdateCustomVisibility(true); } else { updateCustom(data); } }); }) .catch((e) => console.log(e)) .finally(() => { setAddLoading(false); }); }; // 修改自定义参数 const updateCustom = (data) => { setAddLoading(true); fetchPut(`${apiRequest.dataBackup.backupCustom}${row.id}/`, { body: data, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("修改自定义参数成功"); customModalForm.setFieldsValue({ field_k: "", field_v: "", notes: "", }); queryCustom(); fetchData(); setUpdateCustomVisibility(false); setAddModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setAddLoading(false); }); }; // 删除自定义参数 const deleteCustomInfo = (id) => { setAddLoading(true); fetchGet(apiRequest.dataBackup.backupRepeatCustom, { params: { id: id, }, }) .then((res) => { handleResponse(res, (res) => { const repeatName = res.data[0].name; setDeleteRepeatName(repeatName); setDeleteCustomVisibility(true); }); }) .catch((e) => console.log(e)) .finally(() => { setAddLoading(false); }); }; // 删除自定义参数 const deleteCustom = () => { setAddLoading(true); fetchDelete(`${apiRequest.dataBackup.backupCustom}${row.id}/`) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("删除成功"); queryCustom(); fetchData(); setDeleteCustomVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setAddLoading(false); }); }; // 查询可备份实例 const queryCanBackup = () => { setStrategyLoading(true); fetchGet(apiRequest.dataBackup.queryCanBackup) .then((res) => { handleResponse(res, () => { setCanBackupIns(res.data.data); }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 构建添加/修改备份策略的请求体 const makeRequestBody = (data) => { const timeInfo = data.strategy.time.format("HH:mm"); return { backup_instances: data.backup_instances, retain_path: data.retain_path, retain_day: data.retain_day, is_on: data.is_on, backup_custom: data.backup_custom?.map((e) => { return { id: e.key, field_k: e.label[0].props.children[1], field_v: e.label[1], }; }) || [], crontab_detail: { month_of_year: "*", day_of_month: data.strategy.month || "*", day_of_week: data.strategy.week || "*", hour: timeInfo.split(":")[0] || "*", minute: timeInfo.split(":")[1] || "*", }, }; }; // 添加备份策略 const addStrategy = (data) => { setStrategyLoading(true); fetchPost(apiRequest.dataBackup.strategySetting, { body: makeRequestBody(data), }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("添加备份策略成功"); strategyForm.setFieldsValue(strategyFormInit); fetchData(); setStrategyModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 编辑备份策略 const updateStrategy = (data) => { setStrategyLoading(true); fetchPut(`${apiRequest.dataBackup.strategySetting}${strategyRow.id}/`, { body: makeRequestBody(data), }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("修改备份策略成功"); strategyForm.setFieldsValue(strategyFormInit); fetchData(); setStrategyModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 删除备份策略 const deleteStrategy = () => { setLoading(true); fetchDelete(`${apiRequest.dataBackup.strategySetting}${strategyRow.id}/`) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("删除成功"); fetchData(); setDeleteStrategyModal(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 执行备份策略 const executeStrategy = () => { setLoading(true); fetchPost(apiRequest.dataBackup.strategySetting, { body: { id: strategyRow.id, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("任务下发成功"); setExecuteVisible(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchData(); queryCanBackup(); }, []); return (
record.id} noScroll={true} />
提示 } loading={addLoading} onFinish={() => { updateCustom(updateCustomData); }} zIndex={1004} >
该参数{" "} {updateRepeatName} {" "} 实例正在使用
确认修改该参数吗?
提示 } loading={addLoading} onFinish={() => deleteCustom()} zIndex={1004} >
{deleteRepeatName && ( <> 该参数{" "} {deleteRepeatName} {" "} 实例正在使用
)} 确认删除该参数吗?
提示 } loading={loading} onFinish={() => { deleteStrategy(); }} >
确定 删除 该策略吗?
提示 } loading={loading} onFinish={() => { executeStrategy(); }} >
确认对实例{" "} {strategyRow.backup_instances?.join(",")} {" "} 执行备份策略吗?
); }; export default BackupStrategy; ================================================ FILE: omp_web/src/pages/BackupStrategy/index.module.less ================================================ .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .content { padding-left: 40px; padding-top: 30px; display: flex; align-items: center; .label { padding-right: 30px; } } .tips { margin-top: 30px; padding-left: 40px; font-size: 13px; } .saveButtonWrapper { height: 50px; display: flex; align-items: center; justify-content: center; } .saveButton { margin-left: 50%; transform: translateX(-50%); } ================================================ FILE: omp_web/src/pages/DeploymentPlan/config/columns.js ================================================ import moment from "moment"; import { nonEmptyProcessing } from "@/utils/utils"; const getColumnsConfig = (history) => { return [ { title: "编号", width: 140, key: "_idx", dataIndex: "_idx", align: "center", render: nonEmptyProcessing, fixed: "left", }, { title: "模板名称", width: 280, key: "plan_name", dataIndex: "plan_name", align: "center", }, { title: "主机数量", key: "host_num", width: 150, dataIndex: "host_num", align: "center", }, { title: "产品数量", key: "product_num", width: 150, dataIndex: "product_num", align: "center", }, { title: "服务数量", key: "service_num", width: 150, dataIndex: "service_num", align: "center", }, { title: "创建时间", key: "created", dataIndex: "created", align: "center", width: 280, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "创建用户", key: "create_user", dataIndex: "create_user", align: "center", width: 200, render: (text) => { return "Admin"; }, }, { title: "操作", width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/DeploymentPlan/config/models.js ================================================ import React, { useEffect } from "react"; import { useHistory } from "react-router-dom"; import { Button, message, Tooltip, Modal, Steps, Upload, Table } from "antd"; import { SyncOutlined, ImportOutlined, DownloadOutlined, CloudUploadOutlined, CheckCircleFilled, CloseCircleFilled, } from "@ant-design/icons"; import { handleResponse } from "@/utils/utils"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { useState, useRef } from "react"; import XLSX from "xlsx"; const getHeaderRow = (sheet) => { const headers = []; const range = XLSX.utils.decode_range(sheet["!ref"]); let C; const R = range.s.r; for (C = range.s.c; C <= range.e.c; ++C) { const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]; let hdr = "UNKNOWN " + C; if (cell && cell.t) hdr = XLSX.utils.format_cell(cell); headers.push(hdr); } return headers; }; class UploadExcelComponent extends React.Component { state = { loading: false, excelData: { header: null, results: null, }, }; draggerProps = () => { let _this = this; return { name: "file", multiple: false, accept: ".xlsx", maxCount: 1, onRemove() { _this.props.onRemove(); return true; }, onChange(info) { const { status } = info.file; if (status === "done") { //console.log(info.file); message.success(`${info.file.name} 文件解析成功`); } else if (status === "error") { message.error( `${info.file.name} 文件解析失败, 请确保文件内容格式符合规范后重新上传` ); } }, beforeUpload(file, fileList) { //console.log(file); // bmf.md5(file,(err,md5)=>{ // console.log(err,md5,"=====?---") // }) // 校验文件大小 const fileSize = file.size / 1024 / 1024; //单位是M //console.log(fileSize); if (Math.ceil(fileSize) > 10) { message.error("仅支持传入10M以内文件"); return Upload.LIST_IGNORE; } if (!/\.(xlsx)$/.test(file.name)) { message.error("仅支持传入.xlsx文件"); return Upload.LIST_IGNORE; } }, customRequest(e) { _this.readerData(e.file).then( (msg) => { //console.log(e); e.onSuccess(); }, () => { e.onError(); } ); }, }; }; readerData = (rawFile) => { // bmf.md5(rawFile,(err,md5)=>{ // console.log(err,md5,"=====?") // }) return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const data = e.target.result; const workbook = XLSX.read(data, { type: "array" }); // 主机数据 const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const header = getHeaderRow(worksheet); const results = XLSX.utils.sheet_to_json(worksheet); // 服务数据 const sencondSheetName = workbook.SheetNames[1]; const serviceSheet = workbook.Sheets[sencondSheetName]; const serviceHeader = getHeaderRow(serviceSheet); const serviceResults = XLSX.utils.sheet_to_json(serviceSheet); this.generateData( { header, results }, { serviceHeader, serviceResults } ); resolve(); } catch (error) { reject(); } }; reader.readAsArrayBuffer(rawFile); }); }; generateData = ({ header, results }, { serviceHeader, serviceResults }) => { this.setState({ excelData: { header, results }, excelServiceData: { serviceHeader, serviceResults }, }); this.props.uploadSuccess && this.props.uploadSuccess( this.state.excelData, this.state.excelServiceData ); }; render() { return (

点击或将文件拖拽到这里上传

支持扩展名: .xlsx

); } } /* 导入执行计划 */ export const ImportPlanModal = ({ importPlan, setImportPlan }) => { const history = useHistory(); const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState([]); const [serviceDataSource, setServiceDataSource] = useState([]); const [serviceColumns, setServiceColumns] = useState([]); const [tableCorrectData, setTableCorrectData] = useState([]); const [tableErrorData, setTableErrorData] = useState([]); const [tableColumns, setTableColumns] = useState([]); const [serviceTableCorrectData, setServiceTableCorrectData] = useState([]); const [serviceTableErrorData, setServiceTableErrorData] = useState([]); const [serviceTableColumns, setServiceTableColumns] = useState([]); // 涉及数量信息 const [numInfo, setNumInfo] = useState({}); const [stepNum, setStepNum] = useState(0); const [loading, setLoading] = useState(false); // 导入部署步骤状态 const [hostStep, setHostStep] = useState(null); const [serviceStep, setServiceStep] = useState(null); const [importStep, setImportStep] = useState(null); // 导入部署模板状态 const [importResult, setImportResult] = useState(null); // 主机和服务的正确数据 let hostCorrectData = null; let serviceCorrectData = null; // 轮训控制器 const hostAgentTimer = useRef(null); // 失败的columns const errorColumns = [ { title: "行数", key: "row", dataIndex: "row", align: "center", //render: nonEmptyProcessing, width: 60, ellipsis: true, fixed: "left", }, { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", //render: nonEmptyProcessing, width: 120, ellipsis: true, //fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, //fixed: "left" }, { title: "端口", key: "port", dataIndex: "port", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 80, render: (text) => { return ( {text ? text : "-"} ); }, //ellipsis: true, }, { title: "数据分区", key: "data_folder", dataIndex: "data_folder", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 180, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "用户名", key: "username", dataIndex: "username", align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "运行用户", key: "run_user", dataIndex: "run_user", align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "失败原因", key: "validate_error", dataIndex: "validate_error", fixed: "right", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 240, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, ]; // 服务失败的columns const serviceErrorColumns = [ { title: "行数", key: "row", dataIndex: "row", align: "center", width: 60, ellipsis: true, fixed: "left", render: (text) => { if (text < 1) return "-"; return text; }, }, { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", //render: nonEmptyProcessing, width: 120, ellipsis: true, //fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "服务名称", key: "service_name", dataIndex: "service_name", align: "center", //render: nonEmptyProcessing, width: 140, ellipsis: true, //fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, , { title: "运行内存", key: "memory", dataIndex: "memory", align: "center", //render: nonEmptyProcessing, width: 80, ellipsis: true, //fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "失败原因", key: "validate_error", dataIndex: "validate_error", fixed: "right", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 260, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, ]; // 校验主机数据 const fetchBatchValidate = () => { setLoading(true); if (dataSource.length == 0) { message.warning("节点信息中无有效数据,请补充后重新上传"); setHostStep(false); setImportResult(false); return; } let queryBody = dataSource.map((item) => { let result = {}; for (const key in item) { switch (key) { case "IP[必填]": result.ip = item[key]; break; case "实例名[必填]": result.instance_name = item[key]; break; case "密码[必填]": result.password = item[key]; break; case "操作系统[必填]": result.operate_system = item[key]; break; case "数据分区[必填]": result.data_folder = item[key]; break; case "用户名[必填]": result.username = item[key]; break; case "端口[必填]": result.port = item[key]; break; case "运行用户": result.run_user = item[key]; break; case "时间同步服务器": result.use_ntpd = true; result.ntpd_server = item[key]; break; default: break; } } if (!result.use_ntpd) { result.use_ntpd = false; } return { ...result, row: item.key, }; }); // 校验数据 fetchPost(apiRequest.machineManagement.batchValidate, { body: { host_list: queryBody, }, }) .then((res) => { res = res.data; if (res.code == 0) { if (res.data && res.data.error?.length > 0) { setTableErrorData( res.data.error?.map((item, idx) => { return { key: idx, ...item, }; }) ); setTableColumns(errorColumns); setHostStep(false); setImportResult(false); } else { hostCorrectData = res.data.correct?.map((item, idx) => { return { key: idx, ...item, }; }); setHostStep(true); serviceDataValidate(); } } else { message.warning(res.message); setHostStep(false); setImportResult(false); } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 校验服务数据 const serviceDataValidate = () => { setLoading(true); if (serviceDataSource.length == 0) { message.warning("服务分布中无有效数据,请补充后重新上传"); setServiceStep(false); setImportResult(false); return; } // 获取主机实例名数组; let instanceNameArr = dataSource.map((item) => { let instanceName = ""; for (const key in item) { switch (key) { case "实例名[必填]": instanceName = item[key]; break; default: break; } } return instanceName; }); // 获取服务数据 let serviceArr = serviceDataSource.map((item) => { let result = {}; for (const key in item) { switch (key) { case "主机实例名[必填]": result.instance_name = item[key]; break; case "服务名[必填]": result.service_name = item[key]; break; case "运行内存": result.memory = item[key]; break; case "虚拟IP": result.vip = item[key]; break; case "角色": result.role = item[key]; break; case "模式": result.mode = item[key]; break; default: break; } } return { ...result, row: item.key, }; }); // 校验服务分布信息 fetchPost(apiRequest.deloymentPlan.serviceValidate, { body: { instance_name_ls: instanceNameArr, service_data_ls: serviceArr, }, }) .then((res) => { res = res.data; if (res.code == 0) { if (res.data && res.data.error?.length > 0) { setServiceTableErrorData( res.data.error?.map((item, idx) => { return { key: idx, ...item, }; }) ); setServiceTableColumns(serviceErrorColumns); setServiceStep(false); setImportResult(false); } else { serviceCorrectData = res.data.correct?.map((item, idx) => { return { key: idx, ...item, }; }); setServiceStep(true); startDeployment(); } } else { message.warning(res.message); setServiceStep(false); setImportResult(false); } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 导入服务数据 const serviceImport = () => { setLoading(true); let instanceArr = hostCorrectData.map((item) => { return { instance_name: item.instance_name, run_user: item.run_user, }; }); let serviceArr = serviceCorrectData.map((item) => { delete item.key; return { ...item, }; }); fetchPost(apiRequest.deloymentPlan.serviceImport, { body: { instance_info_ls: instanceArr, service_data_ls: serviceArr, }, }) .then((res) => { res = res.data; if (res.code === 0) { setNumInfo(res.data); setImportStep(true); setImportResult(true); // 开始安装 startInstall(res.data.operation_uuid); } else { message.warning( <> {res.message} { message.destroy(); }} > [关闭] , 0 ); setImportStep(false); setImportResult(false); } }) .catch((e) => { console.log(e); }) .finally(() => { setLoading(false); }); }; // 开始部署 const startDeployment = () => { setLoading(true); // 批量导入主机数据 let hostArr = hostCorrectData.map((item) => { delete item.key; return { ...item, }; }); // 纳管主机 fetchPost(apiRequest.machineManagement.batchImport, { body: { host_list: hostArr, }, }) .then((res) => { res = res.data; if (res.code === 0) { serviceImport(); } else { message.warning(res.message); setImportStep(false); setImportResult(false); } }) .catch((e) => console.log(e)) .finally(() => {}); }; // 执行安装任务 const retryInstall = (operation_uuid) => { // 跳转安装页面 fetchPost(apiRequest.appStore.retryInstall, { body: { unique_key: operation_uuid, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { // 跳转安装页面 history.push({ pathname: "/application_management/app_store/installation", state: { uniqueKey: operation_uuid, step: 3, }, }); } }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 查询主机 agent 状态 const queryHostAgent = (operation_uuid) => { let ipArr = hostCorrectData.map((item) => { return item.ip; }); fetchPost(apiRequest.machineManagement.hostsAgentStatus, { body: { ip_list: ipArr, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code === 0 && res.data) { // 调用安装 retryInstall(operation_uuid); // 清除定时器 clearInterval(hostAgentTimer.current); } }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 开始安装 const startInstall = (operation_uuid) => { hostAgentTimer.current = setInterval(() => { queryHostAgent(operation_uuid); }, 1000); }; // 点击导入按钮 const clickImport = () => { setHostStep(null); setServiceStep(null); setImportStep(null); setImportResult(null); setTableCorrectData([]); setTableErrorData([]); setServiceTableCorrectData([]); setServiceTableErrorData([]); setStepNum(1); fetchBatchValidate(); }; useEffect(() => { return () => { // 页面销毁时清除延时器 clearInterval(hostAgentTimer.current); }; }, []); return ( 导入部署模板 } visible={importPlan} footer={null} width={800} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} onCancel={() => { setImportPlan(false); }} afterClose={() => { setDataSource([]); setTableCorrectData([]); setTableErrorData([]); setTableColumns([]); setStepNum(0); setColumns([]); setServiceDataSource([]); setServiceColumns([]); setServiceTableCorrectData([]); setServiceTableErrorData([]); setServiceTableColumns([]); }} destroyOnClose >
下载模版:
上传文件:
{importPlan && ( { setDataSource([]); setColumns([]); setTableCorrectData([]); setTableErrorData([]); setTableColumns([]); setServiceDataSource([]); setServiceColumns([]); setServiceTableCorrectData([]); setServiceTableErrorData([]); setServiceTableColumns([]); }} uploadSuccess={( { results, header }, { serviceHeader, serviceResults } ) => { // 处理主机数据 let dataS = results .filter((item) => { if (item["字段名称(请勿编辑)"]?.includes("请勿编辑")) { return false; } if (!item["实例名[必填]"]) { return false; } return true; }) .map((item, idx) => { return { ...item, key: item.__rowNum__ + 1 }; }); let column = header.filter((item) => { if ( item?.includes("请勿编辑") || item?.includes("UNKNOWN") ) { return false; } return true; }); setDataSource(dataS); setColumns(column); // 处理服务数据 let dataService = serviceResults .filter((item) => { if (item["字段名称(请勿编辑)"]?.includes("请勿编辑")) { return false; } if (!item["主机实例名[必填]"]) { return false; } return true; }) .map((item) => { return { ...item, key: item.__rowNum__ + 1 }; }); setServiceDataSource(dataService); setServiceColumns(serviceHeader); }} /> )}
{stepNum == 1 && ( <>

{hostStep === null && ( <> 正在校验主机数据 ... )} {hostStep === true && ( <> 主机数据校验通过 )} {hostStep === false && ( <> 主机数据校验未通过 )}

{hostStep && (

{serviceStep === null && ( <> 正在校验服务数据 ... )} {serviceStep === true && ( <> 服务数据校验通过 )} {serviceStep === false && ( <> 服务数据校验未通过 )}

)} {serviceStep && (

{importStep === null && ( <> 正在导入模板 ... )} {importStep === true && ( <> 部署模板导入成功 )} {importStep === false && ( <> 部署模板导入失败 )}

)} {tableErrorData.length > 0 && (
0 ? tableErrorData : tableCorrectData } pagination={{ pageSize: 5, }} /> )} {serviceTableErrorData.length > 0 && (
0 ? serviceTableErrorData : serviceTableCorrectData } pagination={{ pageSize: 5, }} /> )} {importStep && ( <>

成功创建 {numInfo.host_num} 台主机

本次共导入 {numInfo.product_num} 个产品, {numInfo.service_num} 个服务

)} {importResult && (

即将进入安装,请稍后 ...

)} {importResult === false && (
)} )} ); }; ================================================ FILE: omp_web/src/pages/DeploymentPlan/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal } from "@/components"; import { Button } from "antd"; import { useState, useEffect, useRef } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; //import updata from "@/store_global/globalStore"; import { useDispatch } from "react-redux"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import getColumnsConfig from "./config/columns"; import { ImportPlanModal } from "./config/models"; import { useHistory } from "react-router-dom"; const DeploymentPlan = () => { const dispatch = useDispatch(); const history = useHistory(); const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [userListSource, setUserListSource] = useState([]); const [searchValue, setSearchValue] = useState(""); const [selectValue, setSelectValue] = useState(); const [labelControl, setLabelControl] = useState("plan_name"); const [instanceSelectValue, setInstanceSelectValue] = useState(""); const [operable, setOperable] = useState(false); // 导入弹框 const [importPlan, setImportPlan] = useState(false); // 不可用弹框 const [disableModal, setDisableModal] = useState(false); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const [showModal, setShowModal] = useState(false); const msgRef = useRef(null); // 获取导入模板按钮是否可操作 function getOpreable() { fetchGet(apiRequest.deloymentPlan.deploymentOperable) .then((res) => { handleResponse(res, (res) => { if (res.code === 0 && res.data === true) { setOperable(true); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.deloymentPlan.deploymentList, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); // 获取按钮是否可操作 getOpreable(); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); //console.log(checkedList) // 防止在校验进入死循环 const flag = useRef(null); return (
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig(history)} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
提示 } noFooter={true} >
已安装服务,快速部署仅在未安装任何服务状态下可用
); }; export default DeploymentPlan; ================================================ FILE: omp_web/src/pages/EmailSettings/index.js ================================================ import { OmpContentWrapper } from "@/components"; import { Spin, Form, Input, Button, InputNumber, message } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import styles from "./index.module.less"; import { MailOutlined } from "@ant-design/icons"; const EmailSettings = () => { const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [form] = Form.useForm(); function fetchData() { setLoading(true); fetchGet(apiRequest.emailSetting.querySetting) .then((res) => { handleResponse(res, (res) => { form.setFieldsValue({ address: res.data.host, port: res.data.port, email: res.data.username, token: res.data.password, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } const multipleUpdate = (data) => { setLoading(true); fetchPost(apiRequest.emailSetting.updateSetting, { body: { host: data.address, port: data.port, username: data.email, password: data.token, }, }) .then((res) => { console.log(res); if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新邮件全局设置成功"); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); fetchData(); }); }; useEffect(() => { fetchData(); }, []); return (
全局设置
); }; export default EmailSettings; ================================================ FILE: omp_web/src/pages/EmailSettings/index.module.less ================================================ .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .saveButtonWrapper { height: 100px; display: flex; align-items: center; justify-content: center; } .saveButton { margin-left: 50%; transform: translateX(-50%); } ================================================ FILE: omp_web/src/pages/ExceptionList/config/columns.js ================================================ import { colorConfig } from "@/utils/utils"; import { Tooltip, Badge } from "antd"; import moment from "moment"; const getColumnsConfig = ( queryRequest, setShowIframe, //updateAlertRead, history, initfilter ) => { return [ { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", width: 200, ellipsis: true, fixed: "left", // sorter: (a, b) => a.instance_name - b.instance_name, // sortDirections: ["descend", "ascend"], render: (text, record) => { return ( {record.instance_name ? record.instance_name : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", //width: 180, ellipsis: true, sorter: (a, b) => a.ip - b.ip, sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { return ( { text && history.push({ pathname: "/resource-management/machine-management", state: { ip: text, }, }); }} > {text} ); }, }, { title: "级别", key: "severity", dataIndex: "severity", align: "center", width: 120, // sorter: (a, b) => a.severity - b.severity, // sortDirections: ["descend", "ascend"], //ellipsis: true, //width:120, usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "critical", text: "严重", }, { value: "warning", text: "警告", }, ], render: (text) => { switch (text) { case "critical": return 严重; case "warning": return 警告; case "info": return 警告; default: return "-"; } }, }, { title: "告警类型", key: "type", dataIndex: "type", // usefilter: true, // queryRequest: queryRequest, // initfilter: initfilter, // filterMenuList: [ // { // value: "service", // text: "服务", // }, // { // value: "host", // text: "主机", // }, // ], align: "center", //ellipsis: true, width: 150, render: (text) => { if (text == "host") { return "主机"; } else if (text == "service") { return "服务"; } else if (text == "component") { return "组件"; } else if (text == "database") { return "数据库"; } }, }, { title: "告警描述", key: "description", dataIndex: "description", align: "center", width: 420, ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "告警时间", //width:180, key: "date", dataIndex: "date", align: "center", //ellipsis: true, sorter: (a, b) => a.date - b.date, sortDirections: ["descend", "ascend"], render: (text) => { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; }, }, { title: "操作", width: 100, key: "", dataIndex: "", fixed: "right", align: "center", render: function renderFunc(text, record, index) { return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/ExceptionList/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpSelect, OmpDrawer, } from "@/components"; import { Button, Select, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import getColumnsConfig from "./config/columns"; import { SearchOutlined } from "@ant-design/icons"; import { useHistory, useLocation } from "react-router-dom"; const ExceptionList = () => { const history = useHistory(); const location = useLocation(); const initIp = location.state?.ip; const initInstanceName = location.state?.instance_name; const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [ipListSource, setIpListSource] = useState([]); const [selectValue, setSelectValue] = useState(initIp); const [instanceSelectValue, setInstanceSelectValue] = useState(initInstanceName); const [searchParams, setSearchParams] = useState({}); // 筛选label const [labelControl, setLabelControl] = useState( initIp ? "ip" : "instance_name" ); const [showIframe, setShowIframe] = useState({}); function fetchData(searchParams = {}) { setLoading(true); fetchGet(apiRequest.ExceptionList.exceptionList, { params: { ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setSearchParams(searchParams); setDataSource( res.data.map((item, idx) => ({ ...item, key: idx + item.ip, })) ); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchIPlist(); }); } const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; useEffect(() => { fetchData({ ip: location.state?.ip, type: location.state?.type, instance_name: location.state?.instance_name, }); }, []); return (
{labelControl === "ip" && ( { fetchData({ ...searchParams, ip: value }); }} /> )} {labelControl === "instance_name" && ( { setInstanceSelectValue(e.target.value); if (!e.target.value) { fetchData({ ...searchParams, instance_name: null, }); } }} onBlur={() => { if (instanceSelectValue) { fetchData({ ...searchParams, instance_name: instanceSelectValue, }); } }} onPressEnter={() => { fetchData({ ...searchParams, instance_name: instanceSelectValue, }); }} suffix={ !instanceSelectValue && ( ) } /> )}
{ if (sorter.columnKey) { let sort = sorter.order == "descend" ? 0 : 1; setTimeout(() => { fetchData({ ...searchParams, ordering: sorter.column ? sorter.columnKey : null, asc: sorter.column ? sort : null, }); }, 200); } }} columns={getColumnsConfig( (params) => { fetchData({ ...searchParams, ...params }); }, setShowIframe, history, location.state?.type )} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {dataSource?.length} {" "} 条

), }} //rowKey={(record) => record.ip} //checkedState={[checkedList, setCheckedList]} />
); }; export default ExceptionList; ================================================ FILE: omp_web/src/pages/HomePage/index.js ================================================ import { apiRequest } from "@/config/requestApi"; import { fetchGet } from "@/utils/request"; import { handleResponse } from "@/utils/utils"; import { Spin } from "antd"; import React, { useEffect, useState } from "react"; import { useHistory } from "react-router-dom"; import styles from "./index.module.less"; import OmpStateBlock from "@/components/OmpStateBlock"; import { OmpProgress } from "@/components"; //import { context } from "@/Root"; import ExceptionList from "./warningList"; import { OmpContentWrapper } from "@/components"; function calcPercentage(normal = 0, total = 1) { const percent = ((normal / total) * 100).toFixed(0); return isNaN(Number(percent)) ? 100 : Number(percent); } const Homepage = () => { const history = useHistory(); const [isLoading, setLoading] = useState(false); const [dataSource, setDataSource] = useState({}); // data数据源,key聚合数据的唯一值 const dataAggregation = (data, key) => { let arr = []; data?.map((i, d) => { let isExistenceArr = arr.filter((e) => e[key] == i[key]); // console.log(isExistenceArr); if (isExistenceArr.length == 0) { arr.push({ [key]: i[key], severity: i.severity, info: [ { ...i, }, ], }); } else { let m = data[d]; // console.log(m); let idx = arr.indexOf(isExistenceArr[0]); arr[idx] = { [key]: i[key], severity: i.severity, info: [ ...arr[idx].info, { ...m, }, ], }; } }); console.log(arr); return arr; }; const queryData = () => { setLoading(true); fetchGet(apiRequest.homepage.instrumentPanel) .then((res) => { handleResponse(res, (res) => { console.log(res); setDataSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { queryData(); }, []); console.log(); return (
{/* */}
状态概览
{/* 应用服务 */}
应用服务状态
dataSource.service?.service_info_all_count && history.push({ pathname: "/application_management/service_management", state: { app_type: "1", }, }) } style={ dataSource.service?.service_info_all_count ? { cursor: "pointer", marginBottom: 2 } : { marginBottom: 2 } } > 服务总数: {dataSource.service?.service_info_all_count}个
{/*
未监控数: {dataSource.service?.service_info_no_monitor_count}个
*/}
0 ? { cursor: "pointer", paddingTop: 10 } : { paddingTop: 10 } } onClick={() => dataSource.service?.service_info_exc_count && history.push({ pathname: "/application-monitoring/exception-list", state: { type: "service", }, }) } > 异常服务: 0 ? { color: "#cf1322" } : {} } > {dataSource.service?.service_info_exc_count}个
{/* 基础组件 */}
基础组件状态
dataSource.component?.component_info_all_count && history.push({ pathname: "/application_management/service_management", state: { app_type: "0", }, }) } style={ dataSource.component?.component_info_all_count ? { cursor: "pointer", marginBottom: 2 } : { marginBottom: 2 } } > 组件实例: {dataSource.component?.component_info_all_count}个
{/*
未监控数: {dataSource.component?.component_info_no_monitor_count}个
*/}
0 ? { cursor: "pointer", paddingTop: 10 } : { paddingTop: 10 } } onClick={() => dataSource.component?.component_info_exc_count && history.push({ pathname: "/application-monitoring/exception-list", state: { type: "component", }, }) } > 异常组件: 0 ? { color: "#cf1322" } : {} } > {dataSource.component?.component_info_exc_count}个
{/* 数据库 */}
数据库状态
dataSource.database?.database_info_all_count && history.push({ pathname: "/application_management/service_management", state: { label_name: "数据库", }, }) } style={ dataSource.database?.database_info_all_count ? { cursor: "pointer", marginBottom: 2 } : { marginBottom: 2 } } > 数据库实例: 0 ? { color: "#1890ff" } : {} } > {dataSource.database?.database_info_all_count}个
{/*
未监控数: {dataSource.database?.database_info_no_monitor_count}个
*/}
0 ? { cursor: "pointer", paddingTop: 10 } : { paddingTop: 10 } } onClick={() => dataSource.database?.database_info_exc_count && history.push({ pathname: "/application-monitoring/exception-list", state: { type: "database", }, }) } > 异常实例: 0 ? { color: "#cf1322" } : {} } > {dataSource.database?.database_info_exc_count}个
{/* 主机状态 */}
主机状态
dataSource.host?.host_info_all_count && history.push({ pathname: "/resource-management/machine-management", }) } style={ dataSource.host?.host_info_all_count ? { cursor: "pointer" } : {} } > 主机总数: {dataSource.host?.host_info_all_count}个
0 ? { cursor: "pointer" } : {} } onClick={() => dataSource.host?.host_info_exc_count && history.push({ pathname: "/application-monitoring/exception-list", state: { type: "host", }, }) } > 异常主机: 0 ? { color: "#cf1322" } : {} } > {dataSource.host?.host_info_exc_count}个
{/* 三方组件 */} {/*
三方组件状态
dataSource.third?.third_info_all_count && history.push({ pathname: "/application_management/service_management", }) } style={ dataSource.third?.third_info_all_count ? { cursor: "pointer", marginBottom: 2 } : { marginBottom: 2 } } > 组件实例: {dataSource.third?.third_info_all_count}个
未监控数: {dataSource.third?.third_info_no_monitor_count}个
0 ? { cursor: "pointer" } : {} } onClick={() => dataSource.third?.third_info_exc_count && history.push({ pathname: "/application-monitoring/exception-list", }) } > 异常实例: 0 ? { color: "#cf1322" } : {} } > {dataSource.third?.third_info_exc_count}个
*/}

异常清单

{ console.log(data); history.push({ pathname: "/application_management/service_management", state: { ip: data?.info[0]?.ip, app_type: "1", }, }); }} criticalLink={(data) => { history.push({ pathname: "/application-monitoring/exception-list", state: { ip: data?.info[0]?.ip, type: "service", }, }); }} title={"应用服务状态"} // hasNotMonitored data={dataAggregation( dataSource.service?.service_info_list, "instance_name" )} />
{ history.push({ pathname: "/application_management/service_management", state: { ip: data?.info[0]?.ip, app_type: "0", }, }); }} criticalLink={(data) => { history.push({ pathname: "/application-monitoring/exception-list", state: { instance_name: data?.info[0]?.instance_name, type: "component", }, }); }} // hasNotMonitored title={"基础组件状态"} data={dataAggregation( dataSource.component?.component_info_list, "instance_name" )} />
{ history.push({ pathname: "/application_management/service_management", state: { ip: data?.info[0]?.ip, label_name: "数据库", }, }); }} criticalLink={(data) => { history.push({ pathname: "/application-monitoring/exception-list", state: { instance_name: data?.info[0]?.instance_name, type: "database", }, }); }} // hasNotMonitored title={"数据库状态"} data={dataAggregation( dataSource.database?.database_info_list, "instance_name" )} />
{ history.push({ pathname: "/resource-management/machine-management", state: { ip: data?.info[0]?.ip, }, }); }} criticalLink={(data) => { history.push({ pathname: "/application-monitoring/exception-list", state: { ip: data?.info[0]?.ip, type: "host", }, }); }} data={dataAggregation(dataSource.host?.host_info_list, "ip")} />
{/*
{ history.push({ pathname: "/application_management/service_management", state: { ip: data.ip, }, }); }} criticalLink={(data) => { history.push({ pathname: "/application-monitoring/exception-list", state: { ip: data.ip, type: "service", }, }); }} hasNotMonitored title={"三方组件状态"} data={dataSource.third?.third_info_list} />
*/}
); }; export default React.memo(Homepage); ================================================ FILE: omp_web/src/pages/HomePage/index.module.less ================================================ .homepageWrapper { //padding: 15px; //padding-top:0px; .pageBlock { //border: 1px solid #DCDEE5; //background-color: aqua; .blockTitle { font-weight: 500; //color: #333; //border:1px solid #DCDEE5; padding: 10px 0 0px 10px; } } } .blockContent { //border: 1px solid #DCDEE5; background-color: #fff; border-radius: 5px; padding: 10px; //margin-bottom: 15px; } .blockOverviewItem { flex: 1; display: flex; justify-content: center; flex-flow: row nowrap; padding-bottom: 10px; padding-top: 5px; border-radius: 5px; & > div:nth-child(1) { margin-right: 15px; align-self: center; & > div:nth-child(1) { width: 80px !important; height: 80px !important; font-size: 16px !important; } } .progressInfo { color: #333; & > div:nth-child(1) { font-size: 13px; font-weight: 500; margin-bottom: 15px; } & > div:nth-child(2) { margin-bottom: 10px; } } } .checkboxGroup { display: flex; justify-content: flex-end; margin-bottom: 10px; // div { // display: flex; // } } .blockItemWrapper { display: flex; flex-flow: row wrap; } .stateButton { width: 180px; margin-right: 10px; margin-bottom: 10px; & > div { max-width: 145px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } } .popContent { & > span { margin-right: 10px; } .ip { display: inline-flex; // todo 这个地方最大宽度不确定,此处为ip地址最大长度,但会导致ip地址较短时页面空一大块 min-width: 120px; } } ================================================ FILE: omp_web/src/pages/HomePage/warningList.js ================================================ import { OmpContentWrapper, OmpTable, OmpDrawer } from "@/components"; import { Tooltip, Badge } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, colorConfig } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import moment from "moment"; import { useHistory } from "react-router-dom"; const ExceptionList = () => { const history = useHistory(); const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [searchParams, setSearchParams] = useState({}); const [showIframe, setShowIframe] = useState({}); const [pageSize, setPageSize] = useState(5); function fetchData(searchParams = {}, noLoading) { !noLoading && setLoading(true); fetchGet(apiRequest.ExceptionList.exceptionList, { params: { ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setSearchParams(searchParams); setDataSource( res.data.map((item, idx) => ({ ...item, key: idx + item.ip, })) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(null, true); }, []); return (
{ setPageSize(e.pageSize); if (sorter.columnKey) { let sort = sorter.order == "descend" ? 0 : 1; setTimeout(() => { fetchData({ ...searchParams, ordering: sorter.column ? sorter.columnKey : null, asc: sorter.column ? sort : null, }); }, 200); } }} columns={getColumnsConfig( (params) => { fetchData({ ...searchParams, ...params }); }, setShowIframe, history )} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["5", "10", "20", "50"], showTotal: () => (

共计{" "} {dataSource?.length} {" "} 条

), pageSize: pageSize, }} //rowKey={(record) => record.ip} //checkedState={[checkedList, setCheckedList]} />
); }; const getColumnsConfig = ( queryRequest, setShowIframe, //updateAlertRead, history ) => { return [ { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", width: 200, ellipsis: true, fixed: "left", sorter: (a, b) => a.instance_name - b.instance_name, sortDirections: ["descend", "ascend"], render: (text, record) => { return ( {record.instance_name ? record.instance_name : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", ellipsis: true, sorter: (a, b) => a.ip - b.ip, sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { return ( { text && history.push({ pathname: "/resource-management/machine-management", state: { ip: text, }, }); }} > {text} ); }, }, { title: "级别", key: "severity", dataIndex: "severity", align: "center", width: 120, // sorter: (a, b) => a.severity - b.severity, // sortDirections: ["descend", "ascend"], //ellipsis: true, //width:120, usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "critical", text: "严重", }, { value: "warning", text: "警告", }, ], render: (text) => { switch (text) { case "critical": return 严重; case "warning": return 警告; case "info": return 警告; default: return "-"; } }, }, { title: "告警类型", key: "type", dataIndex: "type", usefilter: true, queryRequest: queryRequest, // filterMenuList: [ // { // value: "service", // text: "服务", // }, // { // value: "host", // text: "主机", // }, // ], align: "center", //ellipsis: true, width: 150, render: (text) => { if (text == "host") { return "主机"; } else if (text == "service") { return "服务"; } else if (text == "component") { return "组件"; } else if (text == "database") { return "数据库"; } }, }, { title: "告警描述", key: "description", dataIndex: "description", align: "center", width: 420, ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "告警时间", //width:180, key: "date", dataIndex: "date", align: "center", //ellipsis: true, sorter: (a, b) => a.date - b.date, sortDirections: ["descend", "ascend"], render: (text) => { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; }, }, { title: "操作", width: 100, key: "", dataIndex: "", fixed: "right", align: "center", render: function renderFunc(text, record, index) { return ( ); }, }, ]; }; export default ExceptionList; ================================================ FILE: omp_web/src/pages/InstallationRecord/config/ServiceUpgradeModal.js ================================================ import { Button, Modal, Select, Switch } from "antd"; import { useEffect, useRef, useState } from "react"; import { CopyOutlined } from "@ant-design/icons"; //import BMF from "browser-md5-file"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; import { OmpTable } from "@/components"; import { useHistory } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; const ServiceUpgradeModal = ({ vfModalVisibility, setVfModalVisibility, dataSource, installTitle, initLoading, }) => { const uniqueKey = useSelector((state) => state.appStore.uniqueKey); const reduxDispatch = useDispatch(); const [loading, setLoading] = useState(false); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); // console.log(checkedList) //应用服务选择的版本号 const versionInfo = useRef({}); const columns = [ { title: "名称", key: "name", dataIndex: "name", align: "center", ellipsis: true, width: 80, render: (text, record) => { return text; }, }, { title: "升级版本", key: "version", dataIndex: "version", align: "center", ellipsis: true, width: 120, render: (text, record) => { return ( ); }, }, ]; // 高可用是否开启 const [highAvailabilityCheck, setHighAvailabilityCheck] = useState(false); // 批量安装/服务安装选择确认请求 const createInstallInfo = (install_product) => { setLoading(true); fetchPost(apiRequest.appStore.createInstallInfo, { body: { high_availability: highAvailabilityCheck, install_product: install_product, unique_key: uniqueKey, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { //reduxDispatch(getStep1ChangeAction(res.data.data)); } history.push("/application_management/app_store/installation"); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 组件安装 const createComponentInstallInfo = (install_product) => { setLoading(true); fetchPost(apiRequest.appStore.createComponentInstallInfo, { body: { high_availability: highAvailabilityCheck, install_component: install_product, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res.data && res.data.data) { // reduxDispatch(getStep1ChangeAction(res.data.data)); // reduxDispatch(getUniqueKeyChangeAction(res.data.unique_key)); } history.push("/application_management/app_store/installation"); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { // 选中全部 setCheckedList(dataSource.filter((item) => item.is_continue)); }, [dataSource]); // console.log(checkedList) return ( {installTitle == "服务" ? "服务安装-选择版本" : installTitle == "组件" ? "组件安装-选择版本" : "批量安装-选择应用服务"} } afterClose={() => { setCheckedList([]); setHighAvailabilityCheck(false); }} onCancel={() => { setVfModalVisibility(false); }} visible={vfModalVisibility} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
{ return record.name; }} checkedState={[checkedList, setCheckedList]} pagination={false} notSelectable={(record) => ({ // is_continue的不能选中 disabled: !record.is_continue, })} rowSelection={{ selectedRowKeys: checkedList?.map((item) => item?.name), }} />
高可用 { setHighAvailabilityCheck(e); }} />
已选择 {checkedList.length} 个
共 {dataSource.length} 个
); }; export default ServiceUpgradeModal; ================================================ FILE: omp_web/src/pages/InstallationRecord/index.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { useState, useEffect } from "react"; import { useHistory } from "react-router-dom"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { Button } from "antd"; import { handleResponse, _idxInit, nonEmptyProcessing, renderDisc, } from "@/utils/utils"; import moment from "moment"; import ServiceRollbackModal from "../AppStore/config/ServiceRollbackModal"; const renderStatus = (record) => { let text = record.state; if (text.includes("SUCCESS")) { return ( {renderDisc("normal", 7, -1)} {record.state_display} ); } if (text.includes("FAIL")) { return ( {renderDisc("critical", 7, -1)} {record.state_display} ); } if (text.includes("WAIT") || text.includes("ING")) { return ( {renderDisc("warning", 7, -1)} {record.state_display} ); } return "-"; }; const typeMap = { MainInstallHistory: "安装", RollbackHistory: "回滚", UpgradeHistory: "升级", }; const notProhibit = { cursor: "not-allowed", color: "#bbbbbb", }; const InstallationRecord = () => { const history = useHistory(); const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [vfModalVisibility, setVfModalVisibility] = useState(false); const [rowId, setRowId] = useState(""); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "类型", width: 80, key: "module", dataIndex: "module", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], usefilter: true, queryRequest: (params) => { fetchData( { current: 1, pageSize: pagination.pageSize }, pagination.ordering, { ...pagination.searchParams, ...params } ); }, // initfilter: initfilterAppType, filterMenuList: Object.keys(typeMap).map((k) => { return { value: k, text: typeMap[k], }; }), align: "center", fixed: "left", render: (text, record, idx) => { //history.push() return typeMap[text]; }, }, { title: "执行用户", key: "operator", width: 100, dataIndex: "operator", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "状态", key: "install_status", dataIndex: "install_status", width: 100, //sorter: (a, b) => a.is_superuser - b.is_superuser, //sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { return renderStatus(record); }, }, { title: "服务数量", key: "count", dataIndex: "count", width: 60, align: "center", render: nonEmptyProcessing, }, { title: "开始时间", key: "created", dataIndex: "created", align: "center", width: 120, sorter: (a, b) => a.created - b.created, sortDirections: ["descend", "ascend"], render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "结束时间", key: "end_time", dataIndex: "end_time", align: "center", width: 120, render: (text, record) => { if (record.install_status == 1) { return "-"; } if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "用时", key: "duration", dataIndex: "duration", align: "center", width: 120, render: nonEmptyProcessing, }, { title: "操作", key: "1", width: 58, dataIndex: "1", align: "center", fixed: "right", render: function renderFunc(text, record, index) { switch (record.module) { case "MainInstallHistory": return (
{ history.push({ pathname: "/application_management/app_store/installation", state: { uniqueKey: record.module_id, step: 3, }, }); }} style={{ display: "flex", justifyContent: "space-around" }} > 查看
); break; case "RollbackHistory": return (
{ history.push({ pathname: "/application_management/app_store/service_rollback", state: { history: record.module_id, }, }); }} > 查看
); break; case "UpgradeHistory": return ( ); break; default: return "-"; break; } }, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, ordering, searchParams ) { console.log(searchParams); setLoading(true); fetchGet(apiRequest.installHistoryPage.queryAllList, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, search: searchParams?.module, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res); setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, ordering, pagination.searchParams); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default InstallationRecord; ================================================ FILE: omp_web/src/pages/InstallationRecord/indexOld.js ================================================ import { OmpContentWrapper, OmpContentNav } from "@/components"; import { useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import Installation from "./tabs/installation" import Upgrade from "./tabs/upgrade" import Rollback from "./tabs/rollback" const InstallationRecord = () => { const history = useHistory(); const tabKey = useLocation().state?.tabKey const [currentList, setCurrentList] = useState(tabKey || "installation"); const contentNavData = [ { name: "installation", text: "安装记录", handler: () => { if (currentList !== "installation") { setCurrentList("installation"); } }, }, { name: "upgrade", text: "升级记录", handler: () => { if (currentList !== "upgrade") { setCurrentList("upgrade"); } }, }, { name: "backoff", text: "回滚记录", handler: () => { if (currentList !== "backoff") { setCurrentList("backoff"); } }, }, ]; return ( {currentList == "installation" && } {currentList == "upgrade" && } {currentList == "backoff" && } ); }; export default InstallationRecord; ================================================ FILE: omp_web/src/pages/InstallationRecord/tabs/installation.js ================================================ import { OmpTable, OmpContentWrapper } from "@/components"; import { Button } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing, renderDisc, } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; //import updata from "@/store_global/globalStore"; import moment from "moment"; const renderStatus = (text) => { switch (text) { case 0: return {renderDisc("warning", 7, -1)}等待安装; case 1: return {renderDisc("warning", 7, -1)}正在安装; case 2: return {renderDisc("normal", 7, -1)}成功; case 3: return {renderDisc("critical", 7, -1)}失败; case 4: return {renderDisc("notMonitored", 7, -1)}正在注册; default: return "-"; } }; const Installation = ({ history }) => { const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "编号", width: 40, key: "_idx", dataIndex: "_idx", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", fixed: "left", render: (text, record, idx) => { //history.push() return idx + 1 + (pagination.current - 1) * pagination.pageSize; }, }, { title: "执行用户", key: "operator", width: 100, dataIndex: "operator", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "状态", key: "install_status", dataIndex: "install_status", width: 100, //sorter: (a, b) => a.is_superuser - b.is_superuser, //sortDirections: ["descend", "ascend"], align: "center", render: (text) => { return renderStatus(text); }, }, { title: "开始时间", key: "created", dataIndex: "created", align: "center", width: 120, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "结束时间", key: "modified", dataIndex: "modified", align: "center", width: 120, render: (text, record) => { if (record.install_status == 1) { return "-"; } if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "操作", key: "1", width: 58, dataIndex: "1", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return (
{ history.push({ pathname: "/application_management/app_store/installation", state: { uniqueKey: record.operation_uuid, step: 3, }, }); }} style={{ display: "flex", justifyContent: "space-around" }} > 查看
); }, }, ]; //auth/users function fetchData(pageParams = { current: 1, pageSize: 10 }) { setLoading(true); fetchGet(apiRequest.installHistoryPage.queryInstallHistoryList, { params: { page: pageParams.current, size: pageParams.pageSize, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res); setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
{ setTimeout(() => { fetchData(e); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default Installation; ================================================ FILE: omp_web/src/pages/InstallationRecord/tabs/rollback.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { Button } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing, renderDisc, } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import moment from "moment"; const renderStatus = (text) => { switch (text) { case 0: return {renderDisc("warning", 7, -1)}等待回滚; case 1: return {renderDisc("warning", 7, -1)}正在回滚; case 2: return {renderDisc("normal", 7, -1)}回滚成功; case 3: return {renderDisc("critical", 7, -1)}回滚失败; case 4: return {renderDisc("notMonitored", 7, -1)}正在回滚; default: return "-"; } }; const Rollback = ({ history }) => { const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const [vfModalVisibility, setVfModalVisibility] = useState(false); const [serviceList, setServiceList] = useState([]); const columns = [ { title: "编号", width: 40, key: "_idx", dataIndex: "_idx", align: "center", fixed: "left", render: (text, record, idx) => { //history.push() return idx + 1 + (pagination.current - 1) * pagination.pageSize; }, }, { title: "执行用户", key: "operator", width: 100, dataIndex: "operator", align: "center", render: nonEmptyProcessing, }, { title: "服务数量", key: "service_count", width: 60, dataIndex: "service_count", align: "center", render: nonEmptyProcessing, }, { title: "状态", key: "rollback_state", dataIndex: "rollback_state", width: 100, align: "center", render: (text) => { return renderStatus(text); }, }, { title: "回滚时间", key: "created", dataIndex: "created", align: "center", width: 120, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "操作", key: "1", width: 50, dataIndex: "1", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return ( ); }, }, ]; //auth/users function fetchData(pageParams = { current: 1, pageSize: 10 }) { setLoading(true); fetchGet(apiRequest.installHistoryPage.queryRollbackHistoryList, { params: { page: pageParams.current, size: pageParams.pageSize, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res); setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
{ setTimeout(() => { fetchData(e); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
{/* */}
); }; export default Rollback; ================================================ FILE: omp_web/src/pages/InstallationRecord/tabs/upgrade.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { Button } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing, renderDisc, } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import moment from "moment"; import ServiceRollbackModal from "../../AppStore/config/ServiceRollbackModal"; const renderStatus = (text) => { switch (text) { case 0: return {renderDisc("warning", 7, -1)}等待升级; case 1: return {renderDisc("warning", 7, -1)}正在升级; case 2: return {renderDisc("normal", 7, -1)}升级成功; case 3: return {renderDisc("critical", 7, -1)}升级失败; case 4: return {renderDisc("notMonitored", 7, -1)}正在升级; default: return "-"; } }; const notProhibit = { cursor: "not-allowed", color: "#bbbbbb", }; const Upgrade = ({ history }) => { const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [rowId, setRowId] = useState(""); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const [vfModalVisibility, setVfModalVisibility] = useState(false); const [serviceList, setServiceList] = useState([]); const columns = [ { title: "编号", width: 40, key: "_idx", dataIndex: "_idx", align: "center", fixed: "left", render: (text, record, idx) => { //history.push() return idx + 1 + (pagination.current - 1) * pagination.pageSize; }, }, { title: "执行用户", key: "operator", width: 100, dataIndex: "operator", align: "center", render: nonEmptyProcessing, }, { title: "服务数量", key: "service_count", width: 60, dataIndex: "service_count", align: "center", render: nonEmptyProcessing, }, { title: "状态", key: "upgrade_state", dataIndex: "upgrade_state", width: 100, align: "center", render: (text) => { return renderStatus(text); }, }, { title: "升级时间", key: "created", dataIndex: "created", align: "center", width: 120, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, { title: "操作", key: "1", width: 50, dataIndex: "1", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return ( ); }, }, ]; //auth/users function fetchData(pageParams = { current: 1, pageSize: 10 }) { setLoading(true); fetchGet(apiRequest.installHistoryPage.queryUpgradeHistoryList, { params: { page: pageParams.current, size: pageParams.pageSize, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res); setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
{ setTimeout(() => { fetchData(e); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default Upgrade; ================================================ FILE: omp_web/src/pages/Login/index.js ================================================ import { Input, Checkbox, Button, Form } from "antd"; import { useEffect, useState } from "react"; import styles from "./index.module.less"; import img from "@/config/logo/logo.svg"; import { LockOutlined, UserOutlined, CloseCircleFilled, SafetyCertificateOutlined, } from "@ant-design/icons"; import { OmpContentWrapper } from "@/components"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { logout, encrypt } from "@/utils/utils"; import { withRouter } from "react-router"; const Login = withRouter(({ history }) => { const [msgShow, setMsgShow] = useState(false); const [codeError, setCodeError] = useState(false); const [isAutoLogin, setIsAutoLogin] = useState(false); const [loading, setLoading] = useState(false); const [image, setImage] = useState(`/api/users/captcha/?${Math.random()}`); const [form] = Form.useForm(); const onCheckboxChange = (e) => { setIsAutoLogin(e.target.checked); }; function login(data) { setLoading(true); fetchPost(apiRequest.auth.login, { body: { username: encrypt(data.username), password: encrypt(data.password), remember: isAutoLogin, code: data.code, }, }) .then((res) => { if (res.data && res.data.code == 1) { setMsgShow(true); setCodeError(res.data.message === "code error"); } else if (res.data.code == 0) { history.replace({ pathname: "/homepage", state: { data: {}, }, }); //console.log(data) localStorage.setItem("username", data.username); } }) .catch((e) => { console.log(e); }) .finally(() => setLoading(false)); } useEffect(() => { logout("loginPage"); }, []); return (
{/* OMP运维管理平台 */} 运维工具包

用户名密码登录

setMsgShow(false)} > {codeError ? "验证码错误" : "用户名或密码错误"}
{ login(e); }} > } style={{ paddingLeft: 10, width: 360, height: 40 }} placeholder="用户名" /> } style={{ paddingLeft: 10, width: 360, height: 40, marginTop: 10, }} placeholder="密码" /> } suffix={ 无法正常显示 { setImage(`/api/users/captcha/?${Math.random()}`); }} /> } style={{ paddingLeft: 10, width: 360, height: 40, marginTop: 10, }} placeholder="验证码" />
7天自动登录
{" "}
{/*

一站式运维管理平台

*/}
); }); export default Login; ================================================ FILE: omp_web/src/pages/Login/index.module.less ================================================ .loginWrapper { width: 100%; height: 100%; display: flex; justify-content: center; padding-top: 10%; .loginContent { .loginTitle { display: flex; align-items: center; justify-content: center; margin-bottom: 50px; .loginLogo { width: 56px; height: 56px; } .loginOMP { font-size: 25px; padding-left: 10px; font-weight: 600; color: #3a3542; .loginOpenText { font-size: 16px; color: #b4acac; padding-left: 10px; font-weight: 500; } } } .loginInputTitle { color: #99a4a9; font-size: 18px; // animation-duration: 1s; // animation-fill-mode: both; // animation-name: fadeRoute; } .loginMessageShow { opacity: 1; transition: all .2s ease-in; } .loginMessageHide{ opacity: 0; transition: all .2s ease-in; //animation: hide-item 2s ease-in forwards; } // @keyframes hide-item { // 0% {opacity: 1;color:red} // 50% {opacity: .5;color:blue} // 100% {opacity: 0;color:green} // } .loginMessage{ background-color: #99a4a9; } .loginInputWrapper { transition: all .2s ease-in; // div { // padding-bottom: 20px; // } .loginAuto { text-align: right; margin-top: 24px; } } } .loginFooter { color: rgba(0,0,0,.6); text-align: center; margin-top: 60px; } } ================================================ FILE: omp_web/src/pages/LoginLog/index.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { Button, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { SearchOutlined } from "@ant-design/icons"; const LoginLog = () => { const [loading, setLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [selectValue, setSelectValue] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "序号", width: 40, key: "_idx", dataIndex: "_idx", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, fixed: "left", }, { title: "用户名", key: "username", width: 100, dataIndex: "username", sorter: (a, b) => a.username - b.username, sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "IP地址", key: "ip", dataIndex: "ip", width: 100, sorter: (a, b) => a.ip - b.ip, sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "角色", key: "role", dataIndex: "role", width: 100, sorter: (a, b) => a.role - b.role, sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, // { // title: "用户状态", // key: "is_active", // dataIndex: "is_active", // align: "center", // width: 100, // render: (text) => { // if (text) { // return "正常"; // } else { // return "停用"; // } // }, // }, { title: "登录时间", key: "login_time", dataIndex: "login_time", align: "center", width: 100, sorter: (a, b) => a.login_time - b.login_time, sortDirections: ["descend", "ascend"], // render: (text) => { // if (text) { // return moment(text).format("YYYY-MM-DD HH:mm:ss"); // } else { // return "-"; // } // }, render: nonEmptyProcessing, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.operationRecord.queryLoginLog, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
用户名: { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default LoginLog; ================================================ FILE: omp_web/src/pages/MachineManagement/config/columns.js ================================================ import { nonEmptyProcessing, renderDisc, RenderStatusForResult, } from "@/utils/utils"; import { OmpToolTip } from "@/components"; import { DesktopOutlined } from "@ant-design/icons"; import { Dropdown, Menu, Drawer, Tooltip, Spin, Timeline } from "antd"; import moment from "moment"; import styles from "../index.module.less"; import { useSelector } from "react-redux"; import { useRef } from "react"; const colorConfig = { normal: null, warning: "#ffbf00", critical: "#f04134", }; export const DetailHost = ({ isShowDrawer, setIsShowDrawer, loading, data, baseEnv, }) => { // 视口宽度 const viewHeight = useSelector((state) => state.layouts.viewSize.height); // 组件图片字符串 const componentImgStr = ``; const wrapperRef = useRef(null); return ( 主机详细信息面板 IP: {isShowDrawer.record.ip}
} headerStyle={{ padding: "19px 24px", }} placement="right" closable={true} width={`calc(100% - 200px)`} style={{ height: "calc(100%)", // paddingTop: "60px", }} onClose={() => { setIsShowDrawer({ ...isShowDrawer, isOpen: false, }); }} visible={isShowDrawer.isOpen} bodyStyle={{ padding: 10, //paddingLeft:10, backgroundColor: "#e7e9f0", //"#f4f6f8" height: "calc(100%)", }} destroyOnClose={true} >
基本信息
实例名称
{isShowDrawer.record.instance_name}
HOSTNAME
{nonEmptyProcessing(isShowDrawer.record.host_name)}
IP地址
{isShowDrawer.record.ip}
SSH端口
{isShowDrawer.record.port}
用户名
{isShowDrawer.record.username}
系统
{isShowDrawer.record.operate_system}
CPU
{nonEmptyProcessing(isShowDrawer.record.cpu)} c
内存
{nonEmptyProcessing(isShowDrawer.record.memory)} G
硬盘
{isShowDrawer.record.disk ? Object.keys(isShowDrawer.record.disk).map((item) => (
{item} {isShowDrawer.record.disk[item]} G
)) : "-"}
创建时间
{moment(isShowDrawer.record.created).format( "YYYY-MM-DD HH:mm:ss" )}
维护模式
{isShowDrawer.record.is_maintenance ? "是" : "否"}
主机初始化
{renderInitStatue(isShowDrawer.record.init_status)}
Agent状态
主机Agent
{renderStatus(isShowDrawer.record.host_agent)}
监控Agent
{renderStatus(isShowDrawer.record.monitor_agent)}
部署组件信息
部署组件
{isShowDrawer.record.service_num} 个
基础环境
{baseEnv.length > 0 ? ( baseEnv.map((item) => { return (
); }) ) : (
)}
历史记录
{data.map((item) => { return (

[ {item.username}] {item.description}

{moment(item.created).format("YYYY-MM-DD HH:mm:ss")}

); })}
); }; //操作 const renderMenu = ( setUpdateMoadlVisible, setCloseMaintainModal, setOpenMaintainModal, record ) => { return ( setUpdateMoadlVisible(true)}> 修改主机信息 {record.is_maintenance ? ( setCloseMaintainModal(true)}> 关闭维护模式 ) : ( setOpenMaintainModal(true)}> 开启维护模式 )} ); }; const renderStatus = (text) => { switch (text) { case 0: return {renderDisc("normal", 7, -1)}正常; case 1: return {renderDisc("warning", 7, -1)}重启中; case 2: return {renderDisc("critical", 7, -1)}启动失败; case 3: return {renderDisc("warning", 7, -1)}部署中; case 4: return {renderDisc("critical", 7, -1)}部署失败; default: return "-"; } }; const renderInitStatue = (text) => { switch (text) { case 0: return {renderDisc("normal", 7, -1)}成功; case 1: return {renderDisc("notMonitored", 7, -1)}未执行; case 2: return {renderDisc("warning", 7, -1)}执行中; case 3: return {renderDisc("critical", 7, -1)}失败; } }; const getColumnsConfig = ( setIsShowDrawer, setRow, setUpdateMoadlVisible, fetchHostDetail, setCloseMaintainModal, setOpenMaintainModal, setShowIframe, history ) => { return [ { title: "IP地址", key: "ip", dataIndex: "ip", sorter: (a, b) => a.ip - b.ip, sortDirections: ["descend", "ascend"], align: "center", //width: 140, render: (text, record) => { let str = nonEmptyProcessing(text); if (str == "-") { return "-"; } else { return ( { fetchHostDetail(record.id); setIsShowDrawer({ isOpen: true, record: record, }); }} > {str} ); } }, //ellipsis: true, fixed: "left", }, { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "CPU使用率", key: "cpu_usage", dataIndex: "cpu_usage", align: "center", sorter: (a, b) => a.cpu_usage - b.cpu_usage, sortDirections: ["descend", "ascend"], render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, }, { title: "内存使用率", key: "mem_usage", dataIndex: "mem_usage", sorter: (a, b) => a.mem_usage - b.mem_usage, sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, }, { title: "根分区使用率", key: "root_disk_usage", //width:120, dataIndex: "root_disk_usage", align: "center", //ellipsis: true, sorter: (a, b) => a.root_disk_usage - b.root_disk_usage, sortDirections: ["descend", "ascend"], render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, // width:120 }, { title: "数据分区使用率", width: 130, key: "data_disk_usage", dataIndex: "data_disk_usage", align: "center", //ellipsis: true, sorter: (a, b) => a.data_disk_usage - b.data_disk_usage, sortDirections: ["descend", "ascend"], render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, }, { title: "维护模式", key: "is_maintenance", dataIndex: "is_maintenance", align: "center", //ellipsis: true, render: (text) => { if (nonEmptyProcessing(text) == "-") return "-"; return text ? "开" : "关"; }, }, // { // title: "主机初始化", // key: "init_status", // dataIndex: "init_status", // align: "center", // //ellipsis: true, // render: (text) => { // return renderInitStatue(text); // }, // }, { title: "主机Agent", key: "host_agent", dataIndex: "host_agent", align: "center", //ellipsis: true, sorter: (a, b) => a.host_agent - b.host_agent, sortDirections: ["descend", "ascend"], render: (text) => { return renderStatus(text); }, }, { title: "监控Agent", key: "monitor_agent", dataIndex: "monitor_agent", sorter: (a, b) => a.monitor_agent - b.monitor_agent, sortDirections: ["descend", "ascend"], align: "center", //ellipsis: true, render: (text) => { return renderStatus(text); }, }, { title: "服务总数", key: "service_num", dataIndex: "service_num", align: "center", // ellipsis: true, sorter: (a, b) => a.service_num - b.service_num, sortDirections: ["descend", "ascend"], render: (text, record) => { if (text && text !== 0 && text !== "-") { return ( { text && history.push({ pathname: "/application_management/service_management", state: { ip: record.ip, }, }); }} > {text}个 ); } else { if ((!text || text == "-") && text !== 0) { return "-"; } return `${text}个`; } }, }, { title: "告警总数", key: "alert_num", dataIndex: "alert_num", align: "center", //ellipsis: true, sorter: (a, b) => a.alert_num - b.alert_num, sortDirections: ["descend", "ascend"], render: (text, record) => { if (text && text !== 0 && text !== "-") { return ( { text && history.push({ pathname: "/application-monitoring/alarm-log", state: { ip: record.ip, }, }); }} > {text}次 ); } else { if ((!text || text == "-") && text !== 0) { return "-"; } return `${text}次`; } }, }, { title: "操作", //width: 100, width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { if (record?.host_agent == 3 || record?.monitor_agent == 3) { return (
{ setRow(record); }} style={{ display: "flex", justifyContent: "space-around" }} >
监控 更多
); } return (
{ setRow(record); }} style={{ display: "flex" }} >
); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/MachineManagement/config/modals.js ================================================ import React from "react"; import { OmpModal } from "@/components"; import { Button, Input, Select, Form, message, InputNumber, Row, Col, Tooltip, Modal, Steps, Upload, Switch, } from "antd"; import { PlusSquareOutlined, FormOutlined, InfoCircleOutlined, ImportOutlined, DownloadOutlined, CloudUploadOutlined, CheckCircleFilled, CloseCircleFilled, } from "@ant-design/icons"; import { MessageTip, isChineseChar, isNumberChar, isValidIpChar, isExpression, isLetterChar, isSpace, handleResponse, } from "@/utils/utils"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { useState, useRef } from "react"; import star from "./asterisk.svg"; import XLSX from "xlsx"; import { OmpTable } from "@/components"; // import BMF from 'browser-md5-file'; // let bmf = new BMF() // const [modalForm] = Form.useForm(); export const AddMachineModal = ({ loading, visibleHandle, onFinish, createHost, msgInfo, setLoading, }) => { const [modalForm] = Form.useForm(); const [modalLoading, setmodalLoading] = useState(false); const [isOpen, setIsOpen] = useState(true); const timer = useRef(null); const timer2 = useRef(null); return ( 创建主机信息 } form={modalForm} onFinish={(data) => { createHost(data); //onFinish("post", data); }} initialValues={{ data_folder: "/data", port: 22, operate_system: "CentOS", username: "root", use_ntpd: true, }} >
{ if (!value) { return Promise.resolve("success"); } // 校验开头 let startChar = value.slice(0, 1); if ( isNumberChar(startChar) || isLetterChar(startChar) || startChar == "-" ) { if (!isExpression(value)) { if (isChineseChar(value)) { return Promise.reject(`实例名称不支持中文`); } else { if (value.length > 16) { return Promise.resolve("success"); } else { if (isSpace(value)) { return Promise.reject("实例名称不支持空格"); } return new Promise((resolve, rej) => { if (timer.current) { clearTimeout(timer.current); } timer.current = setTimeout(() => { setmodalLoading(true); fetchPost(apiRequest.machineManagement.checkHost, { body: { instance_name: value, }, }) .then((res) => { if (res && res.data) { if (res.data.data) { resolve("success"); } else { rej(`实例名称已存在`); } } }) .catch((e) => console.log(e)) .finally(() => { setmodalLoading(false); }); }, 400); }); } } } else { return Promise.reject("实例名称不支持表情"); } } else { return Promise.reject(`实例名称开头只支持字母、数字或"-"`); } }, }, ]} > 应用组件默认安装到数据分区,请确保具有足够空间 } rules={[ { required: true, message: "请输入数据分区", }, { validator: (rule, value, callback) => { var reg = /[^a-zA-Z0-9\_\-\/]/g; if (!value) { return Promise.resolve("success"); } else { if (value.startsWith("/")) { if (!isChineseChar(value)) { if (!reg.test(value)) { return Promise.resolve("success"); } else { return Promise.reject( `数据分区只支持字母、数字、"/"、"-"和"_"` ); } } else { return Promise.reject( `数据分区只支持字母、数字、"/"、"-"和"_"` ); } } else { return Promise.reject(`数据分区开头必须为"/"`); } } }, }, ]} > IP地址 } useforminstanceinvalidator="true" rules={[ // { // required: true, // message: "请输入IP地址或端口号", // }, { validator: (rule, v, callback) => { let value = modalForm.getFieldValue("IPtext"); let portValue = modalForm.getFieldValue("port"); if (!value) { return Promise.reject("请输入IP地址或端口号"); } if (!portValue) { return Promise.reject("请输入IP地址或端口号"); } if (isValidIpChar(value)) { return new Promise((resolve, rej) => { if (timer2.current) { clearTimeout(timer2.current); } timer2.current = setTimeout(() => { setmodalLoading(true); fetchPost(apiRequest.machineManagement.checkHost, { body: { ip: value, }, }) .then((res) => { if (res && res.data) { if (res.data.data) { resolve("success"); } else { rej(`ip地址已存在`); } } }) .catch((e) => console.log(e)) .finally(() => { setmodalLoading(false); }); }, 600); }); } else { return Promise.reject("请输入正确格式的IP地址"); } }, }, ]} >
: { modalForm.validateFields(["ip"]); }} style={{ width: 82 }} min={1} max={65535} /> 使用 普通用户 纳管主机时,为确保正常安装服务,请 { let a = document.createElement("a"); a.href = apiRequest.machineManagement.downInitScript; document.body.appendChild(a); a.click(); document.body.removeChild(a); }} > 下载 主机初始化脚本,并手动执行 } rules={[ { required: true, message: "请输入用户名", }, { validator: (rule, value, callback) => { var reg = /[^a-zA-Z0-9\_\-]/g; var startReg = /[^a-zA-Z0-9\_]/g; if (value) { let startChar = value.slice(0, 1); if (!startReg.test(startChar)) { if (isChineseChar(value)) { return Promise.reject(`用户名只支持数字、字母、"-"或"_"`); } else { if (!reg.test(value)) { return Promise.resolve("success"); } else { return Promise.reject( `用户名只支持数字、字母、"-"或"_"` ); } } } else { return Promise.reject(`用户名开头只支持数字、字母、或"_"`); } } else { return Promise.resolve("success"); } }, }, ]} > // // // } maxLength={16} placeholder={"请输入用户名"} /> { if (value) { if (!isExpression(value)) { if (isChineseChar(value)) { return Promise.reject("密码不支持中文"); } else { if (value.length < 8) { return Promise.reject("密码长度为8到64位"); } else { if (isSpace(value)) { return Promise.reject("密码不支持空格"); } return Promise.resolve("success"); } } } else { return Promise.reject(`密码不支持输入表情`); } } else { return Promise.resolve("success"); } }, }, ]} > 开启后,将对纳管主机安装ntpdate服务 } > setIsOpen(e)} /> {isOpen && ( { if (value) { if (isValidIpChar(value)) { return Promise.resolve("success"); } else { return Promise.reject("请输入正确格式的IP地址"); } } else { return Promise.resolve("success"); } }, }, ]} > )} ); }; export const UpDateMachineModal = ({ loading, visibleHandle, onFinish, createHost, msgInfo, row, setLoading, }) => { const [modalForm] = Form.useForm(); // console.log(row) const [modalLoading, setmodalLoading] = useState(false); const timer = useRef(null); const timer2 = useRef(null); return ( 修改主机信息 } form={modalForm} onFinish={(data) => { createHost(data); //onFinish("post", data); }} initialValues={{ instance_name: row.instance_name, IPtext: row.ip, data_folder: row.data_folder, port: row.port, operate_system: row.operate_system, username: row.username, ip: row.ip, password: row.password && window.atob(row.password), }} >
{ if (!value) { return Promise.resolve("success"); } // 校验开头 let startChar = value.slice(0, 1); if ( isNumberChar(startChar) || isLetterChar(startChar) || startChar == "-" ) { if (!isExpression(value)) { if (isChineseChar(value)) { return Promise.reject(`实例名称不支持中文`); } else { if (value.length > 16) { return Promise.resolve("success"); } else { if (isSpace(value)) { return Promise.reject("实例名称不支持空格"); } return new Promise((resolve, rej) => { if (timer.current) { clearTimeout(timer.current); } timer.current = setTimeout(() => { setmodalLoading(true); fetchPost(apiRequest.machineManagement.checkHost, { body: { instance_name: value, id: row.id, }, }) .then((res) => { if (res && res.data) { if (res.data.data) { resolve("success"); } else { rej(res.data.message); } } }) .catch((e) => console.log(e)) .finally(() => { setmodalLoading(false); }); }, 400); }); } } } else { return Promise.reject("实例名称不支持表情"); } } else { return Promise.reject(`实例名称开头只支持字母、数字或"-"`); } }, }, ]} > 应用组件默认安装到数据分区,请确保具有足够空间 } rules={[ { required: true, message: "请输入数据分区", }, { validator: (rule, value, callback) => { var reg = /[^a-zA-Z0-9\_\-\/]/g; if (!value) { return Promise.resolve("success"); } else { if (value.startsWith("/")) { if (!isChineseChar(value)) { if (!reg.test(value)) { return Promise.resolve("success"); } else { return Promise.reject( `数据分区只支持字母、数字、"/"、"-"和"_"` ); } } else { return Promise.reject( `数据分区只支持字母、数字、"/"、"-"和"_"` ); } } else { return Promise.reject(`数据分区开头必须为"/"`); } } }, }, ]} > IP地址 } rules={[ { validator: (rule, v, callback) => { let value = modalForm.getFieldValue("IPtext"); let portValue = modalForm.getFieldValue("port"); if (!value) { return Promise.reject("请输入IP地址或端口号"); } if (!portValue) { return Promise.reject("请输入IP地址或端口号"); } if (isValidIpChar(value)) { return new Promise((resolve, rej) => { setmodalLoading(true); fetchPost(apiRequest.machineManagement.checkHost, { body: { ip: value, id: row.id, }, }) .then((res) => { if (res && res.data) { if (res.data.data) { resolve("success"); } else { rej(res.data.message); } } }) .catch((e) => console.log(e)) .finally(() => { setmodalLoading(false); }); }); } else { return Promise.reject("请输入正确格式的IP地址"); } }, }, ]} >
: { modalForm.validateFields(["ip"]); }} /> { var reg = /[^a-zA-Z0-9\_\-]/g; var startReg = /[^a-zA-Z0-9\_]/g; if (value) { let startChar = value.slice(0, 1); if (!startReg.test(startChar)) { if (isChineseChar(value)) { return Promise.reject(`用户名只支持数字、字母、"-"或"_"`); } else { if (!reg.test(value)) { return Promise.resolve("success"); } else { return Promise.reject( `用户名只支持数字、字母、"-"或"_"` ); } } } else { return Promise.reject(`用户名开头只支持数字、字母、或"_"`); } } else { return Promise.resolve("success"); } }, }, ]} > } /> { if (value) { if (!isExpression(value)) { if (isChineseChar(value)) { return Promise.reject("密码不支持中文"); } else { if (value.length < 8) { return Promise.reject("密码长度为8到64位"); } else { if (isSpace(value)) { return Promise.reject("密码不支持空格"); } return Promise.resolve("success"); } } } else { return Promise.reject(`密码不支持输入表情`); } } else { return Promise.resolve("success"); } }, }, ]} > ); }; const getHeaderRow = (sheet) => { const headers = []; const range = XLSX.utils.decode_range(sheet["!ref"]); let C; const R = range.s.r; for (C = range.s.c; C <= range.e.c; ++C) { const cell = sheet[XLSX.utils.encode_cell({ c: C, r: R })]; let hdr = "UNKNOWN " + C; if (cell && cell.t) hdr = XLSX.utils.format_cell(cell); headers.push(hdr); } return headers; }; class UploadExcelComponent extends React.Component { state = { loading: false, excelData: { header: null, results: null, }, }; draggerProps = () => { let _this = this; return { name: "file", multiple: false, accept: ".xlsx", maxCount: 1, onRemove() { _this.props.onRemove(); return true; }, onChange(info) { const { status } = info.file; if (status === "done") { //console.log(info.file); message.success(`${info.file.name} 文件解析成功`); } else if (status === "error") { message.error( `${info.file.name} 文件解析失败, 请确保文件内容格式符合规范后重新上传` ); } }, beforeUpload(file, fileList) { //console.log(file); // bmf.md5(file,(err,md5)=>{ // console.log(err,md5,"=====?---") // }) // 校验文件大小 const fileSize = file.size / 1024 / 1024; //单位是M //console.log(fileSize); if (Math.ceil(fileSize) > 10) { message.error("仅支持传入10M以内文件"); return Upload.LIST_IGNORE; } if (!/\.(xlsx)$/.test(file.name)) { message.error("仅支持传入.xlsx文件"); return Upload.LIST_IGNORE; } }, customRequest(e) { _this.readerData(e.file).then( (msg) => { //console.log(e); e.onSuccess(); }, () => { e.onError(); } ); }, }; }; readerData = (rawFile) => { // bmf.md5(rawFile,(err,md5)=>{ // console.log(err,md5,"=====?") // }) return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = (e) => { try { const data = e.target.result; const workbook = XLSX.read(data, { type: "array" }); const firstSheetName = workbook.SheetNames[0]; const worksheet = workbook.Sheets[firstSheetName]; const header = getHeaderRow(worksheet); const results = XLSX.utils.sheet_to_json(worksheet); //console.log(header, results, "===="); this.generateData({ header, results }); resolve(); } catch (error) { reject(); } }; reader.readAsArrayBuffer(rawFile); }); }; generateData = ({ header, results }) => { this.setState({ excelData: { header, results }, }); this.props.uploadSuccess && this.props.uploadSuccess(this.state.excelData); }; render() { return (

点击或将文件拖拽到这里上传

支持扩展名: .xlsx

); } } /* 批量导入主机 */ export const BatchImportMachineModal = ({ batchImport, setBatchImport, refreshData, }) => { const [dataSource, setDataSource] = useState([]); const [columns, setColumns] = useState([]); // 校验后的表格的colums和dataSource也是不确定的 // 因为不单是在表格展示中需要区分校验成功与否,在这里定义多个数据源用以区分是否成功 const [tableCorrectData, setTableCorrectData] = useState([]); const [tableErrorData, setTableErrorData] = useState([]); const [tableColumns, setTableColumns] = useState([]); const [stepNum, setStepNum] = useState(0); const [loading, setLoading] = useState(false); // 失败的columns const errorColumns = [ { title: "行数", key: "row", dataIndex: "row", align: "center", //render: nonEmptyProcessing, width: 60, ellipsis: true, fixed: "left", }, { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", //render: nonEmptyProcessing, width: 140, ellipsis: true, //fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 140, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, //fixed: "left" }, { title: "端口", key: "port", dataIndex: "port", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 80, render: (text) => { return ( {text ? text : "-"} ); }, //ellipsis: true, }, { title: "数据分区", key: "data_folder", dataIndex: "data_folder", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 180, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "是否安装时间同步", key: "use_ntpd", dataIndex: "use_ntpd", align: "center", width: 120, render: (text) => { return ( {text === false ? "否" : text === true ? "是" : "-"} ); }, ellipsis: true, }, { title: "时间同步服务器", key: "ntpd_server", dataIndex: "ntpd_server", align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "失败原因", key: "validate_error", dataIndex: "validate_error", fixed: "right", // sorter: (a, b) => a.ip - b.ip, // sortDirections: ["descend", "ascend"], align: "center", width: 240, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, ]; // 成功的columns const correctColumns = [ { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", //render: nonEmptyProcessing, width: 140, ellipsis: true, fixed: "left", render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", align: "center", width: 140, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "端口", key: "port", dataIndex: "port", align: "center", width: 80, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "数据分区", key: "data_folder", dataIndex: "data_folder", align: "center", width: 180, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "用户名", key: "username", dataIndex: "username", align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "密码", key: "password", dataIndex: "password", align: "center", width: 130, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "是否安装时间同步", key: "use_ntpd", dataIndex: "use_ntpd", align: "center", width: 120, render: (text) => { return ( {text === false ? "否" : text === true ? "是" : "-"} ); }, ellipsis: true, }, { title: "时间同步服务器", key: "ntpd_server", dataIndex: "ntpd_server", align: "center", width: 120, render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, { title: "系统", key: "operate_system", dataIndex: "operate_system", align: "center", width: 120, fixed: "right", render: (text) => { return ( {text ? text : "-"} ); }, ellipsis: true, }, ]; // 校验数据接口 const fetchBatchValidate = () => { if (dataSource.length == 0) { message.warning( "解析结果中无有效数据,请确保文件内容格式符合规范后重新上传" ); return; } setLoading(true); setTableCorrectData([]); setTableErrorData([]); let queryBody = dataSource.map((item) => { let result = {}; for (const key in item) { switch (key) { case "IP[必填]": result.ip = item[key]; break; case "实例名[必填]": result.instance_name = item[key]; break; case "密码[必填]": result.password = item[key]; break; case "操作系统[必填]": result.operate_system = item[key]; break; case "数据分区[必填]": result.data_folder = item[key]; break; case "用户名[必填]": result.username = item[key]; break; case "端口[必填]": result.port = item[key]; break; case "时间同步服务器": result.use_ntpd = true; result.ntpd_server = item[key]; break; default: break; } } if (!result.use_ntpd) { result.use_ntpd = false; } return { ...result, row: item.key, }; }); // console.log(queryBody) // 校验数据 fetchPost(apiRequest.machineManagement.batchValidate, { body: { host_list: queryBody, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { if (res.data && res.data.error?.length > 0) { setTableErrorData( res.data.error?.map((item, idx) => { return { key: idx, ...item, }; }) ); setTableColumns(errorColumns); } else { setTableCorrectData( res.data.correct?.map((item, idx) => { return { key: idx, ...item, }; }) ); setTableColumns(correctColumns); } setStepNum(1); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 主机创建操作 const fetchBatchImport = () => { let queryBody = tableCorrectData.map((item) => { delete item.key; return { ...item, }; }); setLoading(true); fetchPost(apiRequest.machineManagement.batchImport, { body: { host_list: queryBody, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { setStepNum(2); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; return ( 批量创建主机 } visible={batchImport} footer={null} width={800} loading={loading} // onFinish={(data) => { // createHost(data); // //onFinish("post", data); // }} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} onCancel={() => { setBatchImport(false); }} afterClose={() => { setDataSource([]); setTableCorrectData([]); setTableErrorData([]); setTableColumns([]); setStepNum(0); setColumns([]); refreshData(); }} destroyOnClose >
下载模版:
上传文件:
{batchImport && ( { setDataSource([]); setColumns([]); setTableCorrectData([]); setTableErrorData([]); setTableColumns([]); }} uploadSuccess={({ results, header }) => { //console.log(results, header); let dataS = results .filter((item) => { if (item["字段名称(请勿编辑)"]?.includes("请勿编辑")) { return false; } if (!item["实例名[必填]"]) { return false; } return true; }) .map((item, idx) => { return { ...item, key: item.__rowNum__ + 1 }; }); let column = header.filter((item) => { if ( item?.includes("请勿编辑") || item?.includes("UNKNOWN") ) { return false; } return true; }); setDataSource(dataS); setColumns(column); }} /> )}
{stepNum == 1 && ( <> {tableErrorData.length > 0 ? (

数据校验失败 !

请核对并修改信息后,再重新提交

) : (

数据校验成功 !

)} 0 ? tableErrorData : tableCorrectData } pagination={{ pageSize: 5, }} />
)} {stepNum == 2 && ( <>

主机创建完成 !

本次共创建 {tableCorrectData.length} 台主机

)}
); }; ================================================ FILE: omp_web/src/pages/MachineManagement/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal, OmpSelect, OmpDrawer, } from "@/components"; import { Button, message, Menu, Dropdown } from "antd"; import { useState, useEffect, useRef } from "react"; import { handleResponse, _idxInit, refreshTime } from "@/utils/utils"; import { fetchGet, fetchPost, fetchPatch } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { AddMachineModal, UpDateMachineModal, BatchImportMachineModal, } from "./config/modals"; import { useDispatch } from "react-redux"; import getColumnsConfig, { DetailHost } from "./config/columns"; import { DownOutlined, ExclamationCircleOutlined } from "@ant-design/icons"; import { useHistory, useLocation } from "react-router-dom"; const MachineManagement = () => { const location = useLocation(); const history = useHistory(); const dispatch = useDispatch(); const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //添加弹框的控制state const [addModalVisible, setAddMoadlVisible] = useState(false); //修改弹框的控制state const [updateMoadlVisible, setUpdateMoadlVisible] = useState(false); // 批量导入弹框 const [batchImport, setBatchImport] = useState(false); //选中的数据 const [checkedList, setCheckedList] = useState([]); //table表格数据 const [dataSource, setDataSource] = useState([]); const [ipListSource, setIpListSource] = useState([]); const [selectValue, setSelectValue] = useState(location.state?.ip); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const [isShowDrawer, setIsShowDrawer] = useState({ isOpen: false, src: "", record: {}, }); const [msgShow, setMsgShow] = useState(false); const msgRef = useRef(null); // 定义row存数据 const [row, setRow] = useState({}); // 主机详情历史数据 const [historyData, setHistoryData] = useState([]); // 主机详情基础组件信息 const [baseEnvData, setBaseEnvData] = useState([]); // 主机详情loading const [historyLoading, setHistoryLoading] = useState([]); // 重启主机agent const [restartHostAgentModal, setRestartHostAgentModal] = useState(false); // 重启监控agent const [restartMonterAgentModal, setRestartMonterAgentModal] = useState(false); // 重装主机agent const [reInstallHostAgentModal, setReInstallHostAgentModal] = useState(false); // 重装监控agent const [reInstallMonterAgentModal, setReInstallMonterAgentModal] = useState(false); // 开启维护 const [openMaintainModal, setOpenMaintainModal] = useState(false); // 关闭维护 const [closeMaintainModal, setCloseMaintainModal] = useState(false); // 开启维护(单次) const [openMaintainOneModal, setOpenMaintainOneModal] = useState(false); // 关闭维护(单次) const [closeMaintainOneModal, setCloseMaintainOneModal] = useState(false); // 初始化主机 const [ininHostModal, setIninHostModal] = useState(false); // 删除主机 const [deleteHostModal, setDeleteHostModal] = useState(false); const [showIframe, setShowIframe] = useState({}); // 列表查询 function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.machineManagement.hosts, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { console.log(res.data.results); setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchIPlist(); }); } const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; const createHost = (data) => { setLoading(true); data.ip = data.IPtext; delete data.IPtext; data.port = `${data.port}`; delete data.icon; fetchPost(apiRequest.machineManagement.hosts, { body: { ...data, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("添加主机成功"); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); setAddMoadlVisible(false); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const upDateHost = (data) => { setLoading(true); data.ip = data.IPtext; delete data.IPtext; data.port = `${data.port}`; fetchPatch(`${apiRequest.machineManagement.hosts}${row.id}/`, { body: { ...data, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { // msgRef.current = res.data.message // setMsgShow(true) message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新主机信息成功"); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); setUpdateMoadlVisible(false); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // const fetchHistoryData = (id) => { // setHistoryLoading(true); // fetchGet(apiRequest.machineManagement.operateLog, { // params: { // host_id: id, // }, // }) // .then((res) => { // handleResponse(res, (res) => { // setHistoryData(res.data); // }); // }) // .catch((e) => console.log(e)) // .finally(() => { // setHistoryLoading(false); // }); // }; // 获取主机详情 const fetchHostDetail = (id) => { setHistoryLoading(true); fetchGet(`${apiRequest.machineManagement.hostDetail}${id}`) .then((res) => { handleResponse(res, (res) => { const { deployment_information, history } = res.data; setHistoryData(history); setBaseEnvData(deployment_information); console.log(deployment_information); }); }) .catch((e) => console.log(e)) .finally(() => { setHistoryLoading(false); }); }; // 重启监控agent const fetchRestartMonitorAgent = () => { setLoading(true); fetchPost(apiRequest.machineManagement.restartMonitorAgent, { body: { host_ids: checkedList.map((item) => item.id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("重启监控Agent任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setRestartMonterAgentModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 重启主机agent const fetchRestartHostAgent = () => { setLoading(true); fetchPost(apiRequest.machineManagement.restartHostAgent, { body: { host_ids: checkedList.map((item) => item.id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("重启主机Agent任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setRestartHostAgentModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 重装主机agent const fetchInstallHostAgent = () => { setLoading(true); fetchPost(apiRequest.machineManagement.reInstallHostAgent, { body: { host_ids: checkedList.map((item) => item.id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("重装主机Agent任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setReInstallHostAgentModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 重装监控agent const fetchInstallMonitorAgent = () => { setLoading(true); fetchPost(apiRequest.machineManagement.reInstallMonitorAgent, { body: { host_ids: checkedList.map((item) => item.id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("重装监控Agent任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setReInstallMonterAgentModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 初始化主机 const fetchInitHostAgent = () => { setLoading(true); let hostIdArr = []; hostIdArr = checkedList .filter((item) => { return item.init_status === 1 || item.init_status === 3; }) .map((item) => item.id); if (hostIdArr.length === 0) { setLoading(false); setIninHostModal(false); setCheckedList([]); message.success("所选主机已经初始化完成"); return; } fetchPost(apiRequest.machineManagement.hostInit, { body: { host_ids: hostIdArr, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("初始化主机任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setIninHostModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 主机进入|退出维护模式 const fetchMaintainChange = (e, checkedList) => { let host_arr = []; if (e) { host_arr = checkedList.filter((item) => { return !item.is_maintenance; }); } else { host_arr = checkedList.filter((item) => { return item.is_maintenance; }); } if (host_arr.length == 0) { setLoading(false); setOpenMaintainOneModal(false); setCloseMaintainOneModal(false); setOpenMaintainModal(false); setCloseMaintainModal(false); setCheckedList([]); if (e) { message.success("主机开启维护模式成功"); } else { message.success("主机关闭维护模式成功"); } return; } setLoading(true); fetchPost(apiRequest.machineManagement.hostsMaintain, { body: { is_maintenance: e, host_ids: host_arr.map((item) => item.id), }, }) .then((res) => { if (res.data.code == 0) { if (e) { message.success("主机开启维护模式成功"); } else { message.success("主机关闭维护模式成功"); } } if (res.data.code == 1) { message.warning(res.data.message); } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setOpenMaintainOneModal(false); setCloseMaintainOneModal(false); setOpenMaintainModal(false); setCloseMaintainModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; // 主机删除 const deleteHost = () => { setLoading(true); fetchPost(apiRequest.machineManagement.deleteHost, { body: { host_ids: checkedList.map((item) => item.id), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("删除主机任务已下发"); } if (res.code == 1) { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setDeleteHostModal(false); setCheckedList([]); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }); }; useEffect(() => { fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: location.state?.ip } ); }, []); return (
setOpenMaintainModal(true)} disabled={checkedList.map((item) => item.id).length == 0} > 开启维护模式 { setCloseMaintainModal(true); }} > 关闭维护模式 { setRestartHostAgentModal(true); }} > 重启主机Agent { setRestartMonterAgentModal(true); }} > 重启监控Agent { setReInstallHostAgentModal(true); }} > 重装主机Agent { setReInstallMonterAgentModal(true); }} > 重装监控Agent item.id).length == 0} onClick={() => { setDeleteHostModal(true); }} > 删除主机 {/* item.id).length == 0} onClick={() => { setIninHostModal(true); }} > 初始化主机 */} } placement="bottomCenter" >
IP地址: { // location.state = {} // }} fetchData={(value) => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ip: value }, pagination.ordering ); }} />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig( setIsShowDrawer, setRow, setUpdateMoadlVisible, fetchHostDetail, setCloseMaintainOneModal, setOpenMaintainOneModal, setShowIframe, history )} // notSelectable={(record) => ({ // // 部署中的不能选中 // disabled: record?.host_agent == 3 || record?.monitor_agent == 3, // })} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
{addModalVisible && ( )} {updateMoadlVisible && ( )} 提示 } loading={loading} onFinish={() => { fetchRestartHostAgent(); }} >
确定要重启{" "} {checkedList.length}台 主机{" "} 主机Agent
提示 } loading={loading} onFinish={() => { fetchRestartMonitorAgent(); }} >
确定要重启{" "} {checkedList.length}台 主机{" "} 监控Agent
提示 } loading={loading} onFinish={() => { fetchInstallHostAgent(); }} >
确定要重装{" "} {checkedList.length}台 主机{" "} 主机Agent
提示 } loading={loading} onFinish={() => { fetchInstallMonitorAgent(); }} >
确定要重装{" "} {checkedList.length}台 主机{" "} 监控Agent
提示 } loading={loading} onFinish={() => { fetchMaintainChange(true, checkedList); }} >
确定要对{" "} {checkedList.length}台{" "} 主机下发 开启维护模式 操作?
提示 } loading={loading} onFinish={() => { fetchMaintainChange(false, checkedList); }} >
确定要对{" "} {checkedList.length}台{" "} 主机下发 关闭维护模式 操作?
提示 } loading={loading} onFinish={() => { fetchMaintainChange(true, [row]); }} >
确定要对 当前 主机下发{" "} 开启维护模式 操作?
提示 } loading={loading} onFinish={() => { fetchMaintainChange(false, [row]); }} >
确定要对 当前 主机下发{" "} 关闭维护模式 操作?
提示 } loading={loading} onFinish={() => { fetchInitHostAgent(); }} >
确定要对{" "} {checkedList.length}台 主机{" "} 执行初始化
提示 } loading={loading} onFinish={() => { deleteHost(); }} >
确定要对{" "} {checkedList.length}台 主机{" "} 执行删除命令
{ fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: selectValue }, pagination.ordering ); }} >
); }; export default MachineManagement; ================================================ FILE: omp_web/src/pages/MachineManagement/index.module.less ================================================ .machineManagement { display: flex; } .subMenu { width: 160px; } .warningSearch { display: flex; margin-top: 10px; margin-bottom: 10px; & > div:nth-child(1) { margin-right: 10px; } & > div:last-child { margin-left: auto; } } .antdTableExpandedRow { margin: 0; padding: 0; height: 20px; display: flex; span { margin-left: 60px; } } .redType { background-color: #ff4d4f; color: #fff; border-color:#ff4d4f; } .formItem { margin-bottom: 15px; display: flex; justify-content: center; align-items: center; & > span:nth-child(1) { display: inline-block; width: 100px; font-size: 14px; //font-weight: 500; color: #333; text-align: right; } & > input:nth-child(2) { width: 240px; } } .machineTable { //cursor: url('../../public/conf/logo.svg'),default; cursor: pointer; } .omp_spin_wrapper{ height: calc(100%); } :global { .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover { background-color:#e6f1f6; color:#2e7cee } //悬停样式会覆盖disable样式,在这里把disable权限提高 .ant-dropdown-menu-item-disabled { background-color: #fff!important; color:rgba(0, 0, 0, 0.25)!important } // .ant-drawer .ant-drawer-content { // height: calc(100% - 80px); // } } ================================================ FILE: omp_web/src/pages/MonitoringSettings/index.js ================================================ import { OmpContentWrapper } from "@/components"; import { Button, Input, Form, message, Spin, Switch } from "antd"; import { useState, useEffect } from "react"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchPost, fetchPatch } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { SettingOutlined, MailOutlined } from "@ant-design/icons"; //import updata from "@/store_global/globalStore"; import styles from "./index.module.less"; const MonitoringSettings = () => { const [loading, setLoading] = useState(false); const [pushLoading, setPushLoading] = useState(false); //数据 const [dataSource, setDataSource] = useState([]); const [form] = Form.useForm(); const [pushForm] = Form.useForm(); const [pushIsOpen, setPushIsOpen] = useState(false); //auth/users function fetchData() { setLoading(true); fetchGet(apiRequest.MonitoringSettings.monitorurl) .then((res) => { handleResponse(res, (res) => { // console.log(res.data); res.data.map((item) => { switch (item.name) { case "prometheus": form.setFieldsValue({ prometheus: item.monitor_url, }); return; case "alertmanager": form.setFieldsValue({ alertmanager: item.monitor_url, }); return; case "grafana": form.setFieldsValue({ grafana: item.monitor_url, }); return; default: return; } }); let dir = {}; res.data.map((item) => { dir[item.name] = item.id; }); setDataSource(dir); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } // 查询推送数据 const fetchPushDate = () => { setPushLoading(true); fetchGet(apiRequest.MonitoringSettings.queryPushConfig) .then((res) => { handleResponse(res, (res) => { if (res && res.data) { console.log(res); const { used, server_url } = res.data.email; pushForm.setFieldsValue({ pushIsOpen: used, email: server_url, }); setPushIsOpen(used); } }); }) .catch((e) => console.log(e)) .finally(() => { setPushLoading(false); }); }; const multipleUpdate = (data) => { // console.log(data) const arr = Object.keys(data).map((key) => { return { id: dataSource[key], monitor_url: data[key], }; }); setLoading(true); fetchPatch(apiRequest.MonitoringSettings.multiple_update, { body: { data: arr, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新监控平台配置成功"); fetchData(); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchData(); fetchPushDate(); }, []); // 定义监控地址校验函数 const validateMonitorAddress = (rule, value, callback, title) => { if (value) { var reg = /[^a-zA-Z0-9\-\_\.\~\!\*\'\(\)\;\:\@\&\=\+\$\,\/\?\#\[\]]/g; if (!reg.test(value)) { return Promise.resolve("success"); } else { return Promise.reject(`${title}地址存在非法字符`); } } else { return Promise.resolve("success"); } }; // 改变告警邮件推送 const changePush = (data) => { setPushLoading(true); fetchPost(apiRequest.MonitoringSettings.updatePushConfig, { body: { way_name: "email", env_id: 1, server_url: pushForm.getFieldValue("email"), used: data.pushIsOpen, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新监控推送设置成功"); } } }) .catch((e) => console.log(e)) .finally(() => { setPushLoading(false); fetchPushDate(); }); }; return (
监控平台配置
{ return validateMonitorAddress( rule, value, callback, "Prometheus" ); }, }, ]} > { return validateMonitorAddress( rule, value, callback, "Grafana" ); }, }, ]} > { return validateMonitorAddress( rule, value, callback, "Alert Manager" ); }, }, ]} >
{/* 告警邮件 */}
告警邮件
setPushIsOpen(e)} style={{ borderRadius: "10px" }} /> {pushIsOpen && ( )}
); }; export default MonitoringSettings; ================================================ FILE: omp_web/src/pages/MonitoringSettings/index.module.less ================================================ .wrapper { padding-left: 20px; } .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .saveButtonWrapper { height: 100px; display: flex; align-items: center; justify-content: center; } .saveButton { margin-left: 50%; transform: translateX(-50%); } ================================================ FILE: omp_web/src/pages/PatrolInspectionRecord/config/columns.js ================================================ import { renderDisc, downloadFile, handleResponse } from "@/utils/utils"; import { message } from "antd"; import moment from "moment"; import { apiRequest } from "src/config/requestApi"; import { fetchGet } from "@/utils/request"; const getColumnsConfig = (queryRequest, history, pushData) => { // 推送邮件相关数据 const { pushForm, setPushLoading, setPushAnalysisModal, setPushInfo } = pushData; const fetchDetailData = (id) => { fetchGet(`${apiRequest.inspection.reportDetail}/${id}/`) .then((res) => { handleResponse(res, (res) => { downloadFile(`/download-inspection/${res.data.file_name}`); }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 查询推送数据 const fetchPushDate = (record) => { setPushLoading(true); fetchGet(apiRequest.inspection.queryPushConfig) .then((res) => { handleResponse(res, (res) => { if (res && res.data) { const { to_users } = res.data; pushForm.setFieldsValue({ email: to_users, }); setPushInfo({ id: record.id, module: record.inspection_type, to_users: to_users, }); } }); }) .catch((e) => console.log(e)) .finally(() => { setPushLoading(false); }); }; // 点击推送 const clickPush = (record) => { setPushAnalysisModal(true); fetchPushDate(record); // pushForm.setFieldsValue({ // id: // }) }; return [ { title: "序列", width: 40, key: "idx", dataIndex: "idx", align: "center", fixed: "left", }, { title: "报告名称", width: 120, key: "inspection_name", dataIndex: "inspection_name", align: "center", fixed: "left", render: (text, record, index) => { if (record.inspection_status == 2) { return ( { history?.push({ pathname: `/status-patrol/patrol-inspection-record/status-patrol-detail/${record.id}`, }); }} > {text} ); } return text; }, }, { title: "报告类型", width: 80, key: "inspection_type", align: "center", dataIndex: "inspection_type", usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "service", text: "组件巡检", }, { value: "host", text: "主机巡检", }, { value: "deep", text: "深度分析", }, ], render: (text, record, index) => { if (text == "service") { return "组件巡检"; } if (text == "host") { return "主机巡检"; } if (text == "deep") { return "深度分析"; } }, }, { title: "巡检结果", width: 150, key: "inspection_status", dataIndex: "inspection_status", usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "1", text: "进行中", }, { value: "2", text: "成功", }, { value: "3", text: "失败", }, ], align: "center", render: (text, record, index) => { if (!text && text !== 0) { return "-"; } else if (text === 2) { return
{renderDisc("normal", 7, -1)}成功
; } else if (text === 1) { return
{renderDisc("normal", 7, -1)}进行中
; } else if (text === 3) { return
{renderDisc("critical", 7, -1)}失败
; } else { return text; } }, }, { title: "执行方式", align: "center", dataIndex: "execute_type", key: "execute_type", usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "man", text: "手动", }, { value: "auto", text: "定时", }, ], render: (text) => { if (text == "man") { return "手动执行"; } else if (text == "auto") { return "定时执行"; } else { return "-"; } }, width: 80, }, { title: "巡检时间", width: 200, key: "start_time", dataIndex: "start_time", ellipsis: true, sorter: (a, b) => moment(a.start_time).valueOf() - moment(b.start_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: (text) => { if (!text) return "-"; return moment(text).format("YYYY-MM-DD HH:mm:ss"); }, }, { title: "巡检用时", key: "duration", dataIndex: "duration", render: (text) => { if (text && text !== "-") { let timer = moment.duration(text, "seconds"); let hours = timer.hours(); let hoursResult = hours ? `${hours}小时` : ""; let minutes = timer.minutes(); let minutesResult = minutes % 60 ? `${minutes % 60}分钟` : ""; let seconds = timer.seconds(); let secondsResult = seconds % 60 ? `${seconds % 60}秒` : ""; return `${hoursResult} ${minutesResult} ${secondsResult}`; } else { return "-"; } }, align: "center", width: 60, }, { title: "推送结果", key: "send_email_result", dataIndex: "send_email_result", align: "center", width: 80, render: (text, record) => { switch (text) { case 1: return
{renderDisc("normal", 7, -1)}成功
; case 2: return
{renderDisc("warning", 7, -1)}推送中
; case 0: return
{renderDisc("critical", 7, -1)}失败
; case 3: return
{renderDisc("warning", 7, -1)}未推送
; default: return "-"; } }, }, { title: "操作", width: 60, key: "", dataIndex: "", fixed: "right", align: "center", render: function renderFunc(text, record, index) { return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/PatrolInspectionRecord/config/detail.js ================================================ import "./index.css"; import { columnsConfig, formatTableRenderData, host_port_connectivity_columns, kafka_offsets_columns, kafka_partition_columns, kafka_topic_size_columns, handleResponse, downloadFile, } from "@/utils/utils"; import { Card, Collapse, message, Table, Drawer } from "antd"; import * as R from "ramda"; import { useEffect, useState } from "react"; //import data from "./data.json"; import { useHistory, useLocation } from "react-router-dom"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; const { Panel } = Collapse; const reportColumnConfig = [ { ...columnsConfig.report_service_name, className: "_bigfontSize" }, { ...columnsConfig.report_host_ip, className: "_bigfontSize" }, { ...columnsConfig.report_service_port, className: "_bigfontSize" }, { ...columnsConfig.report_service_status, className: "_bigfontSize" }, { ...columnsConfig.report_cpu_usage, className: "_bigfontSize" }, { ...columnsConfig.report_mem_usage, className: "_bigfontSize" }, { ...columnsConfig.report_run_time, className: "_bigfontSize" }, { ...columnsConfig.report_log_level, className: "_bigfontSize" }, // { ...columnsConfig.report_cluster_name, className: styles._bigfontSize }, ]; export default function PatrolInspectionDetail() { const title = "巡检报告"; const location = useLocation(); const history = useHistory(); // /const data = localStorage.getItem("recordDetailData"); let arr = location.pathname.split("/"); const id = arr[arr.length - 1]; const [drawerVisible, setDrawerVisible] = useState(false); const [drawerText, setDrawerText] = useState(""); const [expandKey, setExpandKey] = useState([]); const [data, setData] = useState({}); const [loading, setLoading] = useState(false); // 是否为主机巡检 const [isHost, setIsHost] = useState(false); const fetchDetailData = (id) => { setLoading(true); fetchGet(`${apiRequest.inspection.reportDetail}/${id}/`) .then((res) => { handleResponse(res, (res) => { setData(res.data); // 通过文件名判断是否为主机巡检 if (res.data.file_name.indexOf("host") === 0) { setIsHost(true); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchDetailData(id); }, []); if (!data) { return
数据暂无
; } return (
{title}
history.push("/status-patrol/patrol-inspection-record") } > 返回
{ message.success( `正在下载巡检报告,双击文件夹中index.html查看报告` ); // download(); downloadFile(`/download-inspection/${data.file_name}`); }} > 导出
( // // )} >
{/* risks存在,且内部数据任一不为空才显示风险指标一栏 */} {data.risks && (!R.isEmpty(data.risks.host_list) || !R.isEmpty(data.risks.service_list)) && ( {data.risks.host_list.length > 0 && (
record.id} columns={[ { ...columnsConfig.report_idx, className: "_bigfontSize", }, { ...columnsConfig.report_host_ip, className: "_bigfontSize", }, { ...columnsConfig.report_system, className: "_bigfontSize", }, { ...columnsConfig.report_risk_level, className: "_bigfontSize", }, { ...columnsConfig.report_risk_describe, className: "_bigfontSize", render: (text) => { return ( { console.log(text); setDrawerText(text); setDrawerVisible(true); }} > {text} ); }, }, { ...columnsConfig.report_resolve_info, className: "_bigfontSize", }, ]} title={() => "主机指标"} dataSource={data.risks.host_list} /> )} {data.risks.service_list.length > 0 && (
record.id} columns={[ { ...columnsConfig.report_idx, className: "_bigfontSize", }, { ...columnsConfig.report_service_name, className: "_bigfontSize", }, { ...columnsConfig.report_host_ip, className: "_bigfontSize", }, { ...columnsConfig.report_service_port, className: "_bigfontSize", }, { ...columnsConfig.report_risk_level, className: "_bigfontSize", }, { ...columnsConfig.report_risk_describe, className: "_bigfontSize", render: (text) => { return ( { console.log(text); setDrawerText(text); setDrawerVisible(true); }} > {text} ); }, }, { ...columnsConfig.report_resolve_info, className: "_bigfontSize", }, ]} title={() => "服务指标"} dataSource={data.risks.service_list} /> )} )} {!R.either(R.isNil, R.isEmpty)(data?.service_topology) && (
{R.addIndex(R.map)((item, index) => { return ( ); }, data?.service_topology)}
)} {!R.either(R.isNil, R.isEmpty)(data.detail_dict?.host) && (
{return styles.didingyi;}} bordered={true} size={"small"} style={{ marginTop: 20 }} scroll={{ x: 1100 }} pagination={false} rowKey={(record, index) => record.id} // defaultExpandAllRows columns={[ { ...columnsConfig.report_idx, className: "_bigfontSize", }, { ...columnsConfig.report_host_ip, className: "_bigfontSize", }, { ...columnsConfig.report_release_version, className: "_bigfontSize", }, { ...columnsConfig.report_host_massage, className: "_bigfontSize", }, { ...columnsConfig.report_cpu_usage, className: "_bigfontSize", }, { ...columnsConfig.report_mem_usage, className: "_bigfontSize", }, { ...columnsConfig.report_disk_usage_root, className: "_bigfontSize", }, { ...columnsConfig.report_disk_usage_data, className: "_bigfontSize", }, { ...columnsConfig.report_run_time, className: "_bigfontSize", }, { ...columnsConfig.report_sys_load, className: "_bigfontSize", }, ]} //expandedRowKeys={expandKey} expandedRowRender={(...arg) => { arg[0].basic = arg[0].basic.filter( (item) => item.name !== "cluster_ip" ); return RenderExpandedContent( ...arg, drawerVisible, setDrawerVisible, drawerText, setDrawerText ); }} // onExpand={(expanded, record) => { // //console.log([...expandKey, record.id]); // setExpandKey([...expandKey, record.id]); // //console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); // }} dataSource={data.detail_dict.host} /> )} {!R.either(R.isNil, R.isEmpty)(data.detail_dict?.database) && (
record.id} // defaultExpandAllRows columns={reportColumnConfig} expandedRowRender={(...arg) => { arg[0].basic = arg[0].basic.filter( (item) => item.name !== "cluster_ip" ); RenderExpandedContent( ...arg, drawerVisible, setDrawerVisible, drawerText, setDrawerText ); }} dataSource={data.detail_dict.database} /> )} {!R.either(R.isNil, R.isEmpty)(data.detail_dict?.component) && (
record.id} // defaultExpandAllRows columns={reportColumnConfig} expandedRowRender={(...arg) => { arg[0].basic = arg[0].basic.filter( (item) => item.name !== "cluster_ip" ); return RenderExpandedContent( ...arg, drawerVisible, setDrawerVisible, drawerText, setDrawerText ); }} dataSource={data.detail_dict.component} /> )} {!R.either(R.isNil, R.isEmpty)(data.detail_dict?.service) && (
record.id} // defaultExpandAllRows columns={reportColumnConfig} expandedRowRender={(...arg) => { arg[0].basic = arg[0].basic.filter( (item) => item.name !== "cluster_ip" ); return RenderExpandedContent( ...arg, drawerVisible, setDrawerVisible, drawerText, setDrawerText ); }} dataSource={data.detail_dict.service} /> )} setDrawerVisible(false)} visible={drawerVisible} width={720} destroyOnClose > {drawerText} ); } function formatTime(text = 0) { let duration = text; const second = Math.round(Number(text)), days = Math.floor(second / 86400), hours = Math.floor((second % 86400) / 3600), minutes = Math.floor(((second % 86400) % 3600) / 60), seconds = Math.floor(((second % 86400) % 3600) % 60); if (days > 0) { duration = days + "天" + hours + "小时" + minutes + "分" + seconds + "秒"; } else if (hours > 0) { duration = hours + "小时" + minutes + "分" + seconds + "秒"; } else if (minutes > 0) { duration = minutes + "分" + seconds + "秒"; } else if (seconds > 0) { duration = seconds + "秒"; } return duration; } // 概览信息 function OverviewItem({ data, type, isHost = false }) { switch (type) { case "task_info": return (
任务信息
任务名称:{data?.task_name}
操作人员:{data?.operator}
任务状态:{data?.task_status === 2 ? "已完成" : "失败"}
); case "time_info": return (
时间统计
开始时间:{data?.start_time}
结束时间:{data?.end_time}
任务耗时:{formatTime(data?.cost)}
); case "scan_info": return (
扫描统计
{data?.host >= 0 &&
主机个数:{data.host}台
} {/* {data?.component >= 0 &&
组件个数:{data.component}个
} */} {data?.service >= 0 &&
服务个数:{data.service}个
}
); case "scan_result": return (
分析结果
总指标数:{data?.all_target_num}
异常指标:{data?.abnormal_target}
{/*
健康度:{data.healthy}
*/}
); } } //平面图 function PlanChart({ title, list, data }) { return (
{title}
{list?.map((item) => { return (
{item}
); })}
); } // Table渲染的子项 // 注:此处需要针对特殊属性渲染额外效果,故将已在table渲染过的属性单独拿出来 function RenderExpandedContent( { basic, host_ip, service_status, run_time, log_level, mem_usage, cpu_usage, service_name, service_port, cluster_name, release_version, host_massage, disk_usage_root, disk_usage_data, sys_load, ...specialProps }, ...arg ) { const { topic_partition, kafka_offsets, topic_size } = specialProps; let [drawerVisible, setDrawerVisible, drawerText, setDrawerText] = arg.slice(-4); const formattedData = Object.entries(specialProps).filter((item) => Array.isArray(item[1]) ); /* eslint-disable */ let deal_host_memory_top_columns = [ { title: "TOP", dataIndex: "TOP", //ellipsis: true, width: 50, className: "_bigfontSize", }, { title: "PID", dataIndex: "PID", //ellipsis: true, align: "center", width: 100, className: "_bigfontSize", }, { title: "使用率", dataIndex: "P_RATE", //ellipsis: true, align: "center", width: 100, className: "_bigfontSize", }, { title: "进程", dataIndex: "P_CMD", ellipsis: true, className: "_bigfontSize", render: (text) => { return ( { setDrawerText(text); setDrawerVisible(true); }} > {text} ); }, }, ]; /* eslint-disable */ const contentMap = { // 主机列表 port_connectivity: { columns: host_port_connectivity_columns, dataSource: specialProps.port_connectivity, title: "端口连通性", }, memory_top: { columns: deal_host_memory_top_columns, dataSource: specialProps.memory_top, title: "内存使用率Top10进程", }, cpu_top: { columns: deal_host_memory_top_columns, dataSource: specialProps.cpu_top, title: "cpu使用率Top10进程", }, kernel_parameters: { columns: [], dataSource: specialProps.kernel_parameters, title: "内核参数", }, // 服务列表 topic_partition: { columns: kafka_partition_columns, dataSource: specialProps.topic_partition, title: "分区信息", }, kafka_offsets: { columns: kafka_offsets_columns, dataSource: specialProps.kafka_offsets, title: "消费位移信息", }, topic_size: { columns: kafka_topic_size_columns, dataSource: specialProps.topic_size, title: "Topic消息大小", }, }; return (
{Array.isArray(basic) && } {formattedData.length > 0 && ( ( // // )} > {formattedData.map((item, idx) => { // 根据当前渲染项,找到对应的content配置数据 const currentContent = contentMap[item[0]]; // 只取目前已经配置了的数据 if (!R.isNil(currentContent)) { return ( {currentContent.columns.length > 0 ? (
record.id} size={"small"} columns={currentContent.columns} dataSource={currentContent.dataSource} pagination={false} /> ) : (
{currentContent.dataSource.map((item, idx) => { return (
{item}
); })}
)} ); } else { // todo 其他数据项 console.log("未配置的数据项", item); } })} )} setDrawerVisible(false)} visible={drawerVisible} width={720} destroyOnClose > {drawerText} ); } // 卡片面板 function BasicCard({ basic }) { return (
{basic.map((item, idx) => (
{item.name_cn}: {formatTableRenderData(JSON.stringify(item.value))}
))}
); } ================================================ FILE: omp_web/src/pages/PatrolInspectionRecord/config/index.css ================================================ .warningSearch { display: flex; justify-content: flex-end; margin-top: 10px; margin-bottom: 10px; } .warningSearch > div:nth-child(1) { margin-right: auto; } .rangePicker { margin-right: 15px; } .contentLeftMenuWrapper { display: flex; } .leftMenuListContent { display: flex; flex-direction: column; font-size: 16px; } .leftMenu { background-color: #fafafa; padding: 15px; margin-top: 10px; margin-right: 10px; height: 82vh; overflow-y: auto; } .trendContentWrapper { width: calc(100% - 0px); padding: 10px; } .title { margin-bottom: 5px; margin-left: 15px; color: #333; font-size: 16px; font-weight: 500; padding: 10px 0; } .trendItemContent { background-color: #fff; border-radius: 10px; } .rangPicker { display: flex; justify-content: flex-end; width: 100%; padding: 10px 60px 10px; } .header { display: flex; align-items: center; padding: 8px 8px 8px 15px; color: #333; border-bottom: 1px solid #bfbfbf; } .header > div:nth-child(2) { display: flex; align-items: center; width: 100%; height: 35px; margin-left: 5px; } .pageInfo { display: flex; justify-content: flex-end; align-items: center; padding: 10px; } .pageInfo > div:nth-child(1) { margin-right: 10px; } .panelItem { background: #edf0f3; border-radius: 4px; border: 0; overflow: hidden; border-bottom: 0 !important; } .panelItem > div:nth-child(1) { padding: 8px 15px; } .panelItem > div:nth-child(2) { background-color: #fff !important; } .reportContent { padding: 15px; } .reportTitle { display: flex; justify-content: space-between; color: #1890ff; margin-bottom: 20px; height: 40px; align-items: center; position: relative; } .reportTitle > div:nth-child(1) { font-size: 22px; font-weight: 500; color: #333; } .reportTitle > div:nth-child(2) { position: absolute; right: 0; display: flex; cursor: pointer; } .reportTitle > div:nth-child(2) { margin-right: 10px; } .overviewItemWrapper { display: flex; justify-content: space-between; flex-flow: wrap; margin: 10px 0; } .overviewItemWrapper > div:nth-child(even) { margin-right: 0; } .overviewItem { display: flex; width: 49.5%; color: #333; margin-bottom: 10px; } .overviewItem > div { border: 1px solid #e8e8e8; padding: 10px; } .overviewItem > div:nth-child(1) { display: flex; justify-content: center; align-items: center; border-right: none; width: 200px; } .overviewItem > div:nth-child(2) { width: 100%; } .planChartWrapper { margin-top: 10px; width: 100%; border: 1px solid #e8e8e8; border-radius: 2px; } .planChartBlockWrapper { display: flex; flex-flow: row wrap; max-height: 240px; overflow-y: auto; padding-top: 15px; padding: 20px; } .stateButton { position: relative; top: 0px; border: 1px solid #333; transition: all 0.2s ease-in-out; width: 178px; margin-right: 32px; margin-bottom: 10px; height: 32px; } .stateButton > div { margin: auto; width: 100%; height: 100%; line-height: 30px; text-align: center; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; } .planChartTitle { background-color: #fafbfd; font-weight: 500; height: 30px; line-height: 30px; border-bottom: solid 1px #e8e8e8; } .planChartTitleCircular { display: inline-block; width: 10px; height: 10px; background-color: #54bba6; border-radius: 50%; margin-right: 10px; margin-left: 20px; } .topologyWrapper { display: flex; align-items: center; margin-right: 80px; margin-bottom: 80px; } .topologyChildren { display: flex; flex-direction: column; } .topologyChildren > div:nth-child(1) { position: relative; } .verticalLine { position: absolute; background-color: #333; left: 0; top: 22px; height: calc(100% - 55px); width: 2px; border-radius: 1px; } .topologyItem { display: flex; justify-content: center; align-items: center; padding: 10px; border: 2px solid #333; border-radius: 5px; } .rootItemBox { display: flex; align-items: center; margin-bottom: 10px; } .connectLine { display: inline-block; width: 30px; background-color: #333; height: 2px; border-radius: 1px; } .basicCardWrapper { display: flex; flex-flow: row wrap; } .basicCardItem { padding: 5px; margin-right: 10px; margin-bottom: 10px; width: 300px; } .buttonContainer { width: 100%; } .greenType { background-color: #5ba165; color: #fff; border-color: #5ba165; } .redType { background-color: #ff4d4f; color: #fff; border-color: #ff4d4f; } .buttonContainer > button { margin-right: 15px; } ._bigfontSize { font-size: 14px; } ================================================ FILE: omp_web/src/pages/PatrolInspectionRecord/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal } from "@/components"; import { Button, message, Input, Checkbox, Form } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import getColumnsConfig from "./config/columns"; import { ExclamationCircleOutlined, SearchOutlined } from "@ant-design/icons"; import { useHistory } from "react-router-dom"; import PatrolInspectionDetail from "@/pages/PatrolInspectionRecord/config/detail"; const PatrolInspectionRecord = () => { const history = useHistory(); const [loading, setLoading] = useState(false); const [pushLoading, setPushLoading] = useState(false); const [instanceSelectValue, setInstanceSelectValue] = useState(""); // 深度分析modal弹框 const [deepAnalysisModal, setDeepAnalysisModal] = useState(false); // 主机巡检modal弹框 const [hostAnalysisModal, setHostAnalysisModal] = useState(false); // 组件巡检modal弹框 const [componenetAnalysisModal, setComponenetAnalysisModal] = useState(false); // 邮件推送modal弹框 const [pushAnalysisModal, setPushAnalysisModal] = useState(false); const [checkboxGroupData, setcheckboxGroupData] = useState([]); // ip列表 const [ipListSource, setIpListSource] = useState([]); // service列表 const [serviceListSource, setServiceListSource] = useState([]); const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 详情数据 const [showDetail, setShowDetail] = useState({ isShow: false, data: {}, }); // 推送表单数据 const [pushForm] = Form.useForm(); // 点击推送按钮数据 const [pushInfo, setPushInfo] = useState(); function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams = {}, ordering ) { setLoading(true); fetchGet(apiRequest.inspection.inspectionList, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((i, idx) => { return { ...i, idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, key: i.id, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } const taskDistribution = (type, data) => { setLoading(true); fetchPost(apiRequest.inspection.taskDistribution, { body: { inspection_name: "mock", inspection_type: type, inspection_status: "1", execute_type: "man", inspection_operator: localStorage.getItem("username"), env: 1, ...data, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("任务已下发"); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { inspection_name: instanceSelectValue }, pagination.ordering ); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setDeepAnalysisModal(false); setHostAnalysisModal(false); setComponenetAnalysisModal(false); }); }; // 巡检的主机ip列表 const fetchIPlist = () => { fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => {}); }; // 巡检的组件列表 const fetchServicelist = () => { fetchGet(apiRequest.inspection.servicesList) .then((res) => { handleResponse(res, (res) => { setServiceListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => {}); }; useEffect(() => { fetchData(); fetchIPlist(); fetchServicelist(); }, []); const pushEmail = () => { setPushLoading(true); fetchPost(apiRequest.inspection.pushEmail, { body: { ...pushInfo, to_users: pushForm.getFieldValue("email"), }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("推送成功"); setPushAnalysisModal(false); fetchData(); } } }) .catch((e) => console.log(e)) .finally(() => setPushLoading(false)); }; if (showDetail.isShow) { return ; } return (
报告名称: { setInstanceSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, inspection_name: null, } ); } }} onBlur={() => { if (instanceSelectValue) { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, inspection_name: instanceSelectValue, } ); } }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, inspection_name: instanceSelectValue, } ); }} suffix={ !instanceSelectValue && ( ) } />
{ console.log("ui"); let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig( (params) => { // console.log(pagination.searchParams) fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, ...params }, pagination.ordering ); }, history, { pushForm, setPushLoading, setPushAnalysisModal, setPushInfo } //fetchDetailData )} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} />
提示 } loading={loading} onFinish={() => { taskDistribution("deep"); }} >
确定要执行 深度分析 操作 ?
{ setcheckboxGroupData([]); }} visibleHandle={[hostAnalysisModal, setHostAnalysisModal]} disabled={checkboxGroupData.length == 0} title={ 主机巡检 } loading={loading} onFinish={() => { taskDistribution("host", { hosts: checkboxGroupData, }); }} > <>
{ //console.log(e.target.checked) if (e.target.checked) { setcheckboxGroupData(ipListSource); } else { setcheckboxGroupData([]); } }} > 全选
{ setcheckboxGroupData(e); }} value={checkboxGroupData} >
{ipListSource.map((item) => { return (
{item}
); })}
{ setcheckboxGroupData([]); }} visibleHandle={[componenetAnalysisModal, setComponenetAnalysisModal]} disabled={checkboxGroupData.length == 0} title={ 组件巡检 } loading={loading} onFinish={() => { taskDistribution("service", { services: checkboxGroupData, }); }} > <>
{ //console.log(e.target.checked) if (e.target.checked) { setcheckboxGroupData( serviceListSource.map((i) => i.service__id) ); } else { setcheckboxGroupData([]); } }} > 全选
{ setcheckboxGroupData(e); }} value={checkboxGroupData} >
{serviceListSource.map((item) => { return (
{item.service__app_name}
); })}
邮件推送} loading={pushLoading} onFinish={() => pushEmail()} >

如果需要配置默认的巡检报告接收人,请点击 history.push({ pathname: "/status-patrol/patrol-strategy", }) } style={{ marginLeft: 4 }} > 这里

); }; export default PatrolInspectionRecord; ================================================ FILE: omp_web/src/pages/PatrolStrategy/index.js ================================================ import { OmpContentWrapper } from "@/components"; import { Switch, Spin, Form, Input, Button, Select, Tooltip, TimePicker, message, } from "antd"; import { useState, useEffect } from "react"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchPost, fetchPut } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import styles from "./index.module.less"; import { SettingOutlined, InfoCircleOutlined, MailOutlined, } from "@ant-design/icons"; import moment from "moment"; const PatrolStrategy = () => { const [loading, setLoading] = useState(false); const [pushLoading, setPushLoading] = useState(false); const [form] = Form.useForm(); const [pushForm] = Form.useForm(); const [dataSource, setDataSource] = useState({}); const [pushDataSource, setPushDataSource] = useState({}); const [isOpen, setIsOpen] = useState(false); const [pushIsOpen, setPushIsOpen] = useState(false); const [frequency, setFrequency] = useState("day"); let weekData = [ "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日", ]; const queryPatrolStrategyData = () => { fetchGet(apiRequest.inspection.queryPatrolStrategy, { params: { job_type: 0, }, }) .then((res) => { if (res && res.data && res.data.data) { setDataSource(res.data.data); let data = res.data.data; let crontab_detail = data.crontab_detail; form.setFieldsValue({ name: { value: data.job_name, }, type: { value: data.job_type + "", }, isOpen: { value: !Boolean(data.is_start_crontab), }, }); if (crontab_detail.day_of_week !== "*") { setFrequency("week"); form.setFieldsValue({ strategy: { frequency: "week", time: moment( `${crontab_detail.hour}:${crontab_detail.minute}`, "HH:mm" ), week: crontab_detail.day_of_week, }, }); } if (crontab_detail.day !== "*") { setFrequency("month"); form.setFieldsValue({ strategy: { frequency: "month", time: moment( `${crontab_detail.hour}:${crontab_detail.minute}`, "HH:mm" ), month: crontab_detail.day, }, }); } if (crontab_detail.day == "*" && crontab_detail.day_of_week == "*") { setFrequency("day"); form.setFieldsValue({ strategy: { frequency: "day", time: moment( `${crontab_detail.hour}:${crontab_detail.minute}`, "HH:mm" ), }, }); } setIsOpen(!Boolean(res.data.data.is_start_crontab)); } }) .catch((e) => { console.log(e); }) .finally(); }; // 修改策略的方法,当前无策略时使用post请求,当前有策略时使用put const changeStrategy = (data) => { let queryData = form.getFieldsValue(); let timeInfo = form.getFieldValue("strategy"); setLoading(true); if (queryData.strategy) timeInfo = queryData.strategy; if (dataSource.job_name) { // 本来有任务,使用更新put fetchPut(apiRequest.inspection.updatePatrolStrategy, { body: { job_type: 0, job_name: queryData.name.value, is_start_crontab: queryData.isOpen.value ? 0 : 1, crontab_detail: { hour: timeInfo.time.format("HH:mm").split(":")[0] || "*", minute: timeInfo.time.format("HH:mm").split(":")[1] || "*", month: "*", day_of_week: timeInfo.week || "*", day: timeInfo.month || "*", }, env: 1, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新巡检策略成功"); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); queryPatrolStrategyData(); }); } else { // 无任务使用post fetchPost(apiRequest.inspection.createPatrolStrategy, { body: { job_type: "0", job_name: queryData.name.value, is_start_crontab: queryData.isOpen.value ? 0 : 1, crontab_detail: { hour: timeInfo.time.format("HH:mm").split(":")[0] || "*", minute: timeInfo.time.format("HH:mm").split(":")[1] || "*", month: "*", day_of_week: timeInfo.week || "*", day: timeInfo.month || "*", }, env: 1, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("新增巡检策略成功"); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); queryPatrolStrategyData(); }); } }; // 查询推送数据 const fetchPushDate = () => { setPushLoading(true); fetchGet(apiRequest.inspection.queryPushConfig) .then((res) => { handleResponse(res, (res) => { if (res && res.data) { const { send_email, to_users } = res.data; pushForm.setFieldsValue({ pushIsOpen: send_email, email: to_users, }); setPushIsOpen(send_email); } }); }) .catch((e) => console.log(e)) .finally(() => { setPushLoading(false); }); }; // 改变推送 const changePush = (data) => { setPushLoading(true); fetchPost(apiRequest.inspection.updatePushConfig, { body: { env_id: 1, to_users: pushForm.getFieldValue("email"), send_email: data.pushIsOpen, }, }) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("更新巡检报告推送设置成功"); } } }) .catch((e) => console.log(e)) .finally(() => { setPushLoading(false); fetchPushDate(); }); }; useEffect(() => { queryPatrolStrategyData(); fetchPushDate(); }, []); return (
巡检设置
{ if (value) { if (value.match(/^[ ]*$/)) { return Promise.reject("请输入巡检任务名称"); } return Promise.resolve("success"); } else { return Promise.resolve("success"); } }, }, ]} > setIsOpen(e)} style={{ borderRadius: "10px" }} /> {isOpen && ( {frequency == "week" && ( )} {frequency == "month" && ( )} )}
{/* 巡检报告推送设置 */}
巡检报告推送设置
setPushIsOpen(e)} style={{ borderRadius: "10px" }} /> {pushIsOpen && ( )}
); }; export default PatrolStrategy; ================================================ FILE: omp_web/src/pages/PatrolStrategy/index.module.less ================================================ .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .content { padding-left: 40px; padding-top: 30px; display: flex; align-items: center; .label { padding-right: 30px; } } .tips { margin-top: 30px; padding-left: 40px; font-size: 13px; } .saveButtonWrapper { height: 100px; display: flex; align-items: center; justify-content: center; } .saveButton { margin-left: 50%; transform: translateX(-50%); } ================================================ FILE: omp_web/src/pages/RuleCenter/index.js ================================================ import { apiRequest } from "@/config/requestApi"; import { fetchGet, fetchPost } from "@/utils/request"; import { handleResponse } from "@/utils/utils"; import { Collapse, Select, Spin, InputNumber, message, Tabs, Button, Tooltip, } from "antd"; import * as R from "ramda"; import { useEffect, useState } from "react"; import styles from "./index.module.less"; import { PlusCircleTwoTone, InfoCircleOutlined, MinusCircleTwoTone, CaretRightOutlined, } from "@ant-design/icons"; const { Panel } = Collapse; const { Option } = Select; const { TabPane } = Tabs; // 保存设置按钮 function SaveSettingsButtonGroup({ saveHandler = () => ({}), disabled = false, style = {}, wrapperStyle = {}, title = "保存", }) { return (
); } // InfoTip function InfoTip({ text }) { return ( ); } const defaultData = [ { condition: ">=", level: "critical", value: 90, }, { condition: ">=", level: "warning", value: 80, }, ]; // 单行指标项 function TargetItem({ data: { name, info, conditionsArr, handler } }) { return (
指标项: {name}
{conditionsArr.map((item, idx) => { return (
阈值: `${value}%`} parser={(value) => value.replace("%", "")} onChange={(val) => { const foo = R.clone(conditionsArr); foo[idx] = R.assoc("value", val, foo[idx]); handler(foo); }} /> 级别: {conditionsArr.length === 1 && ( { const foo = R.clone(conditionsArr); const index_type = foo[0].index_type; const old_value = parseInt(foo[0].value); if (foo[0].level === "critical") { foo.push({ condition: ">=", level: "warning", value: old_value - 10 > 0 ? old_value - 10 : 0, index_type, }); } else { foo.unshift({ condition: ">=", level: "critical", value: old_value + 10 > 100 ? 100 : old_value + 10, index_type, }); } handler(foo); }} style={{ fontSize: 18, marginRight: 20, }} /> )} {idx === 1 && ( { const foo = R.clone(conditionsArr); foo.splice(1, 1); handler(foo); }} style={{ fontSize: 18 }} twoToneColor="#f5222d" /> )}
); })}
); } function RuleCenter() { const [cpuUsed, setCpuUsed] = useState([0, 1]); const [memoryUsed, setMemoryUsed] = useState([0, 1]); const [diskRootUsed, setDiskRootUsed] = useState([0, 1]); const [diskDataUsed, setDiskDataUsed] = useState([0, 1]); const [serviceActive, setServiceActive] = useState([0, 1]); const [serviceCpuUsed, setServiceCpuUsed] = useState([0, 1]); const [serviceMemoryUsed, setServiceMemoryUsed] = useState([0, 1]); const [kafkaData, setKafkaData] = useState([ { index_type: "kafka_consumergroup_lag", condition: ">=", value: 5000, level: "critical", }, { index_type: "kafka_consumergroup_lag", condition: ">=", value: 3000, level: "warning", }, ]); const machineTargetsMap = [ { name: "cpu_used", info: `主机当前“CPU”使用率`, conditionsArr: cpuUsed, handler: (val) => setCpuUsed(val), }, { name: "memory_used", info: `主机当前“内存”使用率`, conditionsArr: memoryUsed, handler: (val) => setMemoryUsed(val), }, { name: "disk_root_used", info: `主机当前“根分区”使用率`, conditionsArr: diskRootUsed, handler: (val) => setDiskRootUsed(val), }, { name: "disk_data_used", info: `主机当前“数据分区”使用率`, conditionsArr: diskDataUsed, handler: (val) => setDiskDataUsed(val), }, ]; const serviceTargetsMap = [ { name: "service_active", info: `服务当前“是否存活”,验证标准是端口是否可以连通`, conditionsArr: serviceActive, handler: (val) => setServiceActive(val), }, { name: "service_cup_used", info: `服务当前“CPU”使用率`, conditionsArr: serviceCpuUsed, handler: (val) => setServiceCpuUsed(val), }, { name: "service_memory_used", info: `服务当前“内存”使用率`, conditionsArr: serviceMemoryUsed, handler: (val) => setServiceMemoryUsed(val), }, ]; const [isMachineLoading, setMachineLoading] = useState(false); const [isServiceLoading, setServiceLoading] = useState(false); const [isCustomizationLoading, setCustomizationLoading] = useState(false); function fetchHostDate() { setMachineLoading(true); fetchGet(apiRequest.ruleCenter.hostThreshold, { params: { env_id: 1, }, }) .then((res) => { handleResponse(res, (res) => { const { data: { cpu_used, memory_used, disk_root_used, disk_data_used }, } = res.dat; setCpuUsed(cpu_used.length > 0 ? cpu_used : defaultData); setMemoryUsed(memory_used.length > 0 ? memory_used : defaultData); setDiskRootUsed( disk_root_used.length > 0 ? disk_root_used : defaultData ); setDiskDataUsed( disk_data_used.length > 0 ? disk_data_used : defaultData ); }); }) .catch((e) => console.log(e)) .finally(() => { setMachineLoading(false); }); } function fetchServiceDate() { setServiceLoading(true); fetchGet(apiRequest.ruleCenter.serviceThreshold, { params: { env_id: 1, }, }) .then((res) => { handleResponse(res, (res) => { const { data: { service_active, service_cpu_used, service_memory_used }, } = res.data; setServiceActive( service_active.length > 0 ? service_active : [ { index_type: "service_active", condition: "==", value: "False", level: "critical", }, ] ); setServiceCpuUsed( service_cpu_used.length > 0 ? service_cpu_used : defaultData ); setServiceMemoryUsed( service_memory_used.length > 0 ? service_memory_used : defaultData ); }); }) .catch((e) => console.log(e)) .finally(() => { setServiceLoading(false); }); } function fetchCustomDate() { setCustomizationLoading(true); fetchGet(apiRequest.ruleCenter.queryCustomThreshold, { params: { env_id: 1, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code === 0 && Object.keys(res.data).length !== 0) { setKafkaData( res.data?.kafka?.kafka_consumergroup_lag?.map((item) => { // 把其中的value改成number return { ...item, value: Number(item.value) }; }) ); } }); }) .catch((e) => console.log(e)) .finally(() => { setServiceLoading(false); }); } function fetchData() { setMachineLoading(true); setServiceLoading(true); setCustomizationLoading(true); Promise.all([ fetchGet(apiRequest.ruleCenter.hostThreshold, { params: { env_id: 1, }, }), fetchGet(apiRequest.ruleCenter.serviceThreshold, { params: { env_id: 1, }, }), fetchGet(apiRequest.ruleCenter.queryCustomThreshold, { params: { env_id: 1, }, }), ]) .then(([hostResponse, serviceResponse, customThresholdRes]) => { hostResponse = hostResponse.data; serviceResponse = serviceResponse.data; customThresholdRes = customThresholdRes.data; if (hostResponse.code === 3) { message.warn("登录已过期,请重新登录"); localStorage.clear(); window.__history__.replace("/login"); return; } const { data: { cpu_used, memory_used, disk_root_used, disk_data_used }, } = hostResponse; const { data: { service_active, service_cpu_used, service_memory_used }, } = serviceResponse; setCpuUsed(cpu_used.length > 0 ? cpu_used : defaultData); setMemoryUsed(memory_used.length > 0 ? memory_used : defaultData); setDiskRootUsed( disk_root_used.length > 0 ? disk_root_used : defaultData ); setDiskDataUsed( disk_data_used.length > 0 ? disk_data_used : defaultData ); setServiceActive( service_active.length > 0 ? service_active : [ { index_type: "service_active", condition: "==", value: "False", level: "critical", }, ] ); setServiceCpuUsed( service_cpu_used.length > 0 ? service_cpu_used : defaultData ); setServiceMemoryUsed( service_memory_used.length > 0 ? service_memory_used : defaultData ); if ( customThresholdRes.code === 0 && Object.keys(customThresholdRes.data).length !== 0 ) { setKafkaData( customThresholdRes.data?.kafka?.kafka_consumergroup_lag?.map( (item) => { // 把其中的value改成number return { ...item, value: Number(item.value) }; } ) ); } }) .catch((e) => console.log(e)) .finally(() => { setMachineLoading(false); setServiceLoading(false); setCustomizationLoading(false); }); } useEffect(() => { fetchData(); }, []); function isThresholdAccurate(data) { console.log(data); const invalidData = R.filter((item) => { const critical = R.find(R.propEq("level", "critical"), item) || {}; const warning = R.find(R.propEq("level", "warning"), item) || {}; return Number(critical.value) <= Number(warning.value); }, data); //判断其中同一条指标项的程度不能重复 for (let item of Object.values(data)) { if (item.length == 2 && item[0].level == item[1].level) { message.warn( `请检查${item[0].index_type}的阈值触发规则,不能设置相同级别` ); return; } } //判断级别不能相等 const checkDataAgain = (data) => { let arr = Object.values(data).filter((item) => item.length == 2); let result = arr.filter((item) => item[0].level == item[1].level); return result; }; if (!R.isEmpty(invalidData)) { const type = R.values(invalidData)[0][0].index_type; message.warn(`请检查${type}的阈值触发规则,严重应该大于警告`); return; } else { if (checkDataAgain(data).length !== 0) { const type = checkDataAgain(data)[0][0].index_type; message.warn(`请检查${type}的阈值触发规则,严重应该大于警告`); return; } return true; } } const checkKafkaData = (data) => { if (data.length == 1) return; if (data[0].level == data[1].level) return "请检查kafka_consumergroup_lag的阈值触发规则,不能设置相同级别"; let [criticalItem] = data.filter((item) => item.level === "critical"); let [warningItem] = data.filter((item) => item.level === "warning"); if (criticalItem.value <= warningItem.value) return "请检查kafka_consumergroup_lag的阈值触发规则,严重应该大于警告"; }; return (
( )} >
{machineTargetsMap.map((item, idx) => { return ( item.handler(val)} key={`machine-${idx}`} data={item} /> ); })}
{ const update_data = { cpu_used: cpuUsed, memory_used: memoryUsed, disk_root_used: diskRootUsed, disk_data_used: diskDataUsed, }; // 如果核验数据未通过,直接退出 if (isThresholdAccurate(update_data)) { setMachineLoading(true); fetchPost(apiRequest.ruleCenter.hostThreshold, { body: { update_data: update_data, env_id: 1, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("更新主机指标成功"); fetchHostDate(); } }); }) .catch((e) => console.log(e)) .finally(() => { setMachineLoading(false); }); } }} />
{serviceTargetsMap.map((item, idx) => { if (item.name === "service_active") { //服务状态单独展示 const { name, info, conditionsArr, handler } = item; return (
指标项: {name}
服务: 级别:
); } else { return ( item.handler(val)} data={item} /> ); } })}
{ const update_data = { service_active: serviceActive, service_cpu_used: serviceCpuUsed, service_memory_used: serviceMemoryUsed, }; // 不检查service_active,因为只有一项 if ( isThresholdAccurate({ service_cpu_used: serviceCpuUsed, service_memory_used: serviceMemoryUsed, }) ) { setServiceLoading(true); fetchPost(apiRequest.ruleCenter.serviceThreshold, { body: { update_data: update_data, env_id: 1, }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("更新服务指标成功"); fetchServiceDate(); } }); }) .catch((e) => console.log(e)) .finally(() => { setServiceLoading(false); }); } }} />
{/* 定制化指标 */}
指标项: kafka_consumergroup_lag
{kafkaData.map((item, idx) => { return (
阈值: `${value}%`} // parser={(value) => value.replace("%", "")} precision={0} onChange={(val) => { const copyData = [...kafkaData]; copyData[idx].value = val; setKafkaData(copyData); }} /> 级别: {kafkaData.length === 1 && ( { const foo = R.clone(kafkaData); const index_type = foo[0].index_type; if (foo[0].level == "critical") { foo.push({ condition: ">=", level: "warning", value: foo[0].value - 1000 > 0 ? foo[0].value - 1000 : 0, index_type, }); } else { foo.unshift({ condition: ">=", level: "critical", value: foo[0].value + 1000, index_type, }); } setKafkaData(foo); }} style={{ fontSize: 18, marginRight: 20, }} /> )} {idx === 1 && ( { const foo = R.clone(kafkaData); foo.splice(1, 1); setKafkaData(foo); }} style={{ fontSize: 18 }} theme={"twoTone"} twoToneColor="#f5222d" /> )}
); })} {/* */}
{ let checkMessage = checkKafkaData(kafkaData); if (checkMessage) { message.warn(checkMessage); } else { setCustomizationLoading(true); fetchPost(apiRequest.ruleCenter.queryCustomThreshold, { body: { //update_data: update_data, env_id: 1, service_name: "kafka", index_type: "kafka_consumergroup_lag", index_type_info: [...kafkaData], }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("更新定制化指标成功"); fetchCustomDate(); } }); }) .catch((e) => console.log(e)) .finally(() => { setCustomizationLoading(false); }); } }} />
); } export default RuleCenter; ================================================ FILE: omp_web/src/pages/RuleCenter/index.module.less ================================================ .warningSearch { display: flex; margin-top: 10px; margin-bottom: 10px; & > div:nth-child(1) { margin-right: 10px; } & > div:last-child { margin-left: auto; } } .tabItemWrapper { //background-color: #f5f5f5; .tabTitle { color: #333; height: 35px; margin-top: 15px; display: flex; align-items: center; background-color: #f5f5f5; padding-left: 10px; font-size: 14px; font-weight: 500; } .tabContent { padding: 15px 15px 15px 60px; & > div { margin-bottom: 20px; } .tabItemTitle { color: #333; font-weight: 500; display: inline-flex; align-items: center; } .inlineTabItemTitle { color: #333; font-weight: 500; margin-bottom: 10px; display: inline-flex; align-items: center; justify-content: flex-end; width: 100px; } .inlineTabItemSwitch { display: inline-block; position: relative; top: -1px; } .patrolSettingWrapper { display: flex; align-items: center; & > div:nth-child(1) { align-self: flex-start; margin-top: 5px; } .chooseInterval { margin-right: 10px; } } } } .tabItemNotice { font-size: 14px; color: #8c8c8c; margin-top: 5px; margin-bottom: 15px; } .panelItem { background: "#f5f5f5"; border-radius: 4px; border: 0; overflow: "hidden"; border-bottom: 0 !important; & > div:nth-child(1) { padding: 8px 15px; } // 覆盖展开后的content背景色 & > div:nth-child(2) { background-color: #fff !important; } .targetItemWrapper { display: flex; flex-direction: column; padding-top: 15px; & > div { margin-bottom: 10px; } } } .targetItem { padding-left: 60px; margin-bottom: 10px; display: flex; min-height: 30px; } .itemTitle { color: #333; width: 250px; min-width: 250px; font-weight: 500; margin-top: -8px; } .conditionItemWrapper { display: flex; flex-flow: row wrap; } .conditionItem { display: flex; align-items: center; height: 32px; margin-bottom: 10px; } .compsItemWrapper { display: flex; flex-flow: row wrap; align-items: center; margin-bottom: 20px; & > div { margin-bottom: 0 !important; } } .saveButtonWrapper { height: 100px; display: flex; align-items: center; justify-content: center; } ================================================ FILE: omp_web/src/pages/RuleExtend/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpModal, OmpMessageModal, } from "@/components"; import { Button, Input, Form, message, Tooltip, InputNumber, Modal, Upload, Table, } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, renderDisc, downloadFile, } from "@/utils/utils"; import { fetchGet, fetchDelete, fetchPut } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { SearchOutlined, QuestionCircleOutlined, ExclamationCircleOutlined, ImportOutlined, UploadOutlined, FormOutlined, CloseOutlined, DownloadOutlined, } from "@ant-design/icons"; import { useHistory } from "react-router-dom"; import axios from "axios"; import star from "./asterisk.svg"; const RuleExtend = () => { const [loading, setLoading] = useState(false); const [modalForm] = Form.useForm(); const [upDateForm] = Form.useForm(); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); const [row, setRow] = useState({}); // 删除操作 const [deleteVisible, setDeleteVisible] = useState(false); // 行内删除操作 const [deleteRowVisible, setDeleteRowVisible] = useState(false); // 添加操作控制器 const [addVisible, setAddVisible] = useState(false); // 修改操作控制器 const [upDateVisible, setUpDateVisible] = useState(false); // 查询操作控制器 const [statusVisible, setStatusVisible] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [selectValue, setSelectValue] = useState(); // detail数据 const [detailList, setDetailList] = useState([]); // 选中的绑定主机 const [executionData, setExecutionData] = useState([]); // 绑定主机控制器 const [hostListVisible, setHostListVisible] = useState(false); // 是否展示执行绑定主机校验信息 const [isShowErrMsg, setIsShowErrMsg] = useState(false); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 主机翻页数据 const [hostPagination, setHostPagination] = useState({ current: 1, pageSize: 10, total: 0, }); // 主机列表 const [hostList, setHostList] = useState([]); const renderStatus = (text) => { switch (text) { case 0: return {renderDisc("normal", 7, -1)}正常; case 1: return {renderDisc("warning", 7, -1)}重启中; case 2: return {renderDisc("critical", 7, -1)}启动失败; case 3: return {renderDisc("warning", 7, -1)}部署中; case 4: return {renderDisc("critical", 7, -1)}部署失败; default: return "-"; } }; const columns = [ { title: "描述", // width: 60, key: "description", dataIndex: "description", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", width: 220, fixed: "left", ellipsis: true, }, { title: ( 绑定主机 {" "} ), key: "bound_hosts_num", width: 120, dataIndex: "bound_hosts_num", align: "center", }, { title: "探测周期(s)", key: "scrape_interval", dataIndex: "scrape_interval", width: 120, align: "center", }, { title: "操作", width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return ( ); }, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.ruleCenter.queryExtendRuleList, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } /* 限制数字输入框只能输入整数 */ const limitNumber = (value) => { if (typeof value === "string") { return !isNaN(Number(value)) ? value.replace(/^(0+)|[^\d]/g, "") : ""; } else if (typeof value === "number") { return !isNaN(value) ? String(value).replace(/^(0+)|[^\d]/g, "") : ""; } else { return ""; } }; // 添加规则 const addExtend = (data) => { setLoading(true); let formData = new FormData(); formData.append("file", data.collectionScript.file.originFileObj); formData.append("scrape_interval", data.scrape_interval); const config = { headers: { "Content-Type": "multipart/form-data;boundary=" + new Date().getTime(), }, }; axios .post(apiRequest.ruleCenter.queryExtendRuleList, formData, config) .then(function (response) { console.log(response); if (response && response.data && response.data.code == 1) { message.warning(response.data.message); } else { message.success(`添加操作下发成功`); } }) .catch(function (error) { console.log(error); }) .finally(() => { setLoading(false); setAddVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; // 删除规则 function deleteRule(id) { setLoading(true); fetchDelete(`${apiRequest.ruleCenter.queryExtendRuleList}${id}/`) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success(`删除操作下发成功`); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setDeleteRowVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); } // 查询扩展指标 const queryExpansionIndex = (id) => { setLoading(true); fetchGet(apiRequest.ruleCenter.queryDetail, { params: { id, }, }) .then((res) => { handleResponse(res, (res) => { setDetailList(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 请求主机列表hostPagination, setHostPagination] const queryHostList = (pageParams = { current: 1, pageSize: 10 }) => { setLoading(true); fetchGet(apiRequest.machineManagement.hosts, { params: { page: pageParams.current, size: pageParams.pageSize, }, }) .then((res) => { handleResponse(res, (res) => { // 设置表格的默认选中 // setCheckedList(executionData); setCheckedList( res.data.results.filter((item) => { let result = executionData.filter((i) => i.ip == item.ip); return result.length !== 0; }) ); setHostList(res.data.results); setHostPagination({ ...hostPagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 修改接口 const upDateRule = (data, id) => { setLoading(true); fetchPut(`${apiRequest.ruleCenter.queryExtendRuleList}${id}/`, { body: data, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success(`修改操作下发成功`); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setUpDateVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; useEffect(() => { fetchData(pagination); }, []); return (
{/* */} {/* {}} disabled={checkedList.map((item) => item.id).length == 0} > 禁用 {}} > 启用 } placement="bottomCenter" > */}
描述: { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, description: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, description: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, description: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} // rowKey={(record) => record.id} // checkedState={[checkedList, setCheckedList]} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
提示 } loading={loading} onFinish={() => { deleteRule(row.id); // statusUpdate([row.id], 1) // deleteQuota(row); }} >
确定要对 {row.description}{" "} 指标 下发删除命令
{/* 提示 } loading={loading} onFinish={() => { // statusUpdate([row.id], 1) // deleteQuota(row); }} >
确定要对 {checkedList.length}{" "} 条指标 下发删除命令
*/} {/* 添加规则 */} 添加扩展指标 } form={modalForm} onFinish={(data) => { console.log(data); addExtend(data); }} initialValues={{ scrape_interval: 60, }} >
{ const fileSize = file.size / 1024 / 1024; //单位是mb if (Math.ceil(fileSize) > 2) { message.error("仅支持传入2MB以内文件"); return Upload.LIST_IGNORE; } // return Upload.LIST_IGNORE; }} customRequest={(e) => { // console.log(e) // 直接调用成功,在整个表单提交的时候去携带文件流 e.onSuccess(); }} > {" "}
{/* 查询 */} 查看扩展指标状态} loading={loading} >
{ if (!text) { return "-"; } return {text}; }, }, { title: "状态", key: "status", dataIndex: "status", align: "center", width: 120, // fixed: "right", render: (text) => { if (text === "down") { return "停用"; } return "正常"; }, }, { title: "采集用时", key: "last_scrape_duration", dataIndex: "last_scrape_duration", align: "center", width: 120, render: (text) => { if (!text) { return "-"; } return `${(text * 1000).toFixed(2)}ms`; }, // fixed: "right", }, { title: "错误信息", key: "last_error", dataIndex: "last_error", align: "center", width: 220, ellipsis: true, render: (text) => { if (!text) { return "-"; } return {text}; }, fixed: "right", }, ]} dataSource={detailList} /> {/* 修改规则 */} 修改扩展指标 } form={upDateForm} beForeOk={() => { if (executionData.length == 0) { // performTasks(); setIsShowErrMsg(true); } }} afterClose={() => { setExecutionData([]); }} onFinish={(data) => { console.log(data); if (executionData.length !== 0) { // performTasks(); console.log("通过"); upDateRule( { description: data.description, scrape_interval: data.scrape_interval, bound_hosts: executionData.map((i) => i.ip), }, row.id ); } }} initialValues={{ scrape_interval: 60, }} >
{" "} 请选择绑定主机
} label={ 绑定主机 } >
{executionData.map((i) => ( ))}
{ setCheckedList([]); }} onCancel={() => { setHostListVisible(false); }} visible={hostListVisible} footer={null} //width={1000} loading={loading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
{ return (
{text}
); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, { title: "主机Agent", key: "host_agent", dataIndex: "host_agent", align: "center", width: 120, //ellipsis: true, render: (text) => { return renderStatus(text); }, }, ]} onChange={(e, filters, sorter) => { setTimeout(() => { queryHostList(e); }, 200); }} dataSource={hostList} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {hostPagination.total} {" "} 条

), ...hostPagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
); }; const ExecutionTargetItem = ({ info, executionData, setIsShowErrMsg }) => { const [data, setData] = executionData; return (
{info.ip} { let result = data.filter((i) => !i.id == info.id); if (result.length == 0) { setIsShowErrMsg(true); } setData(() => { return result; }); }} />
); }; export default RuleExtend; ================================================ FILE: omp_web/src/pages/RuleIndicator/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpModal, OmpMessageModal, } from "@/components"; import { Button, Input, Form, message, Menu, Dropdown, Select, Radio, Cascader, Tooltip, InputNumber, Switch, Table, } from "antd"; import { useState, useEffect, useRef } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost, fetchDelete } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { SearchOutlined, DownOutlined, PlusSquareOutlined, QuestionCircleOutlined, ExclamationCircleOutlined, } from "@ant-design/icons"; import { useHistory } from "react-router-dom"; const RuleIndicator = () => { const [loading, setLoading] = useState(false); const [modalForm] = Form.useForm(); const [upDateForm] = Form.useForm(); const history = useHistory(); //选中的数据 const [checkedList, setCheckedList] = useState([]); const [row, setRow] = useState({}); // 测试展示数据 const [testQueryResults, setTestQueryResults] = useState([]); // 测试弹框控制器 const [testVisible, setTestVisible] = useState(false); // 批量停用弹框控制器 const [stopVisible, setStopVisible] = useState(false); // 单独停用弹框控制器 const [stopRowVisible, setStopRowVisible] = useState(false); // 批量启用弹框控制器 const [startVisible, setStartVisible] = useState(false); // 单独启用弹框控制器 const [startRowVisible, setStartRowVisible] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [selectValue, setSelectValue] = useState(); // 添加规则控制器 const [addMoadlVisible, setAddMoadlVisible] = useState(false); // 修改规则控制器 const [upDateVisible, setUpDateVisible] = useState(false); // 删除规则控制器 const [deleteMoadlVisible, setDeleteMoadlVisible] = useState(false); // 规则类型 const [ruleType, setRuleType] = useState("0"); // 持续时长单位 const [forTimeCompany, setForTimeCompany] = useState("s"); // 选择内置规则联级数据 const [cascaderOption, setCascaderOption] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "规则名称", // width: 60, key: "alert", dataIndex: "alert", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", width: 250, fixed: "left", ellipsis: true, }, { title: "对比规则", key: "compare_str", width: 120, dataIndex: "compare_str", align: "center", }, { title: "阈值", key: "threshold_value", dataIndex: "threshold_value", width: 120, align: "center", }, { title: "持续时长(s)", key: "for_time", dataIndex: "for_time", width: 120, align: "center", }, { title: "级别", key: "severity", dataIndex: "severity", align: "center", width: 120, render: (text) => { const map = { warning: "警告", critical: "严重", }; return map[text]; }, }, { title: "状态", key: "status", dataIndex: "status", align: "center", width: 120, render: (text) => { const map = ["已停用", "已启用"]; return map[text]; }, }, { title: "指标类型", key: "quota_type", dataIndex: "quota_type", align: "center", width: 120, render: (text) => { const map = ["内置指标", "自定义promsql"]; return map[text]; }, }, { title: "操作", width: 150, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return (
{ console.log(record); setRow(record); }} >
{ // // 持续时长单位 // setForTimeCompany("s"); queryBuiltinsQuota(); setUpDateVisible(true); setForTimeCompany( record.for_time[record.for_time.length - 1] ); if (record.quota_type == 0) { setRuleType("0"); upDateForm.setFieldsValue({ quota_type: "0", alert: record.alert, builtins_quota: [record.service, record.name], compare_str: record.compare_str, threshold_value: record.threshold_value, for_time: record.for_time.substring( 0, record.for_time.length - 1 ), severity: record.severity, status: record.status, }); } else if (record.quota_type == 1) { setRuleType("1"); upDateForm.setFieldsValue({ quota_type: "1", alert: record.alert, expr: record.expr, compare_str: record.compare_str, threshold_value: record.threshold_value, for_time: record.for_time.substring( 0, record.for_time.length - 1 ), service: record.service, severity: record.severity, status: record.status, summary: record.summary, description: record.description, }); } }} > 修改 { if (record.status == 1) { setStopRowVisible(true); } else { setStartRowVisible(true); } }} > {record.status == 1 ? "停用" : "启用"} { if (record.forbidden && record.forbidden == 1) { setDeleteMoadlVisible(true); } }} > 删除
); }, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.ruleCenter.queryPromemonitor, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } const dictionaries = useRef(null); // 请求内置规则的选择指标联级配置 const queryBuiltinsQuota = () => { fetchGet(apiRequest.ruleCenter.queryBuiltinsQuota) .then((res) => { handleResponse(res, (res) => { if (res.data) { dictionaries.current = res.data; let data = res.data; setCascaderOption(() => { return Object.keys(data).map((key) => { let children = data[key].map((item) => { console.log(item); return { label: item.name, value: item.name, disabled: item.name == "数据分区使用率" ? true : false, // JSON.stringify({ // description: item.description, // name: item.name, // expr: item.expr, // service: item.service, // }), }; }); return { value: key, label: key, children, }; }); }); } }); }) .catch((e) => console.log(e)) .finally(() => {}); }; const addQuota = (data) => { let queryData = {}; if (data.quota_type === "0") { let builtins_quota = data.builtins_quota; let result = dictionaries.current[builtins_quota[0]].filter( (f) => f.name == builtins_quota[1] )[0]; queryData = { threshold_value: data.threshold_value, compare_str: data.compare_str, for_time: `${data.for_time}${forTimeCompany}`, severity: data.severity, alert: data.alert, status: data.status ? 1 : 0, quota_type: Number(data.quota_type), builtins_quota: { description: result.description, name: result.name, expr: result.expr, service: result.service, }, }; } else if (data.quota_type === "1") { queryData = { summary: data.summary, description: data.description, expr: data.expr, service: data.service, threshold_value: data.threshold_value, compare_str: data.compare_str, for_time: `${data.for_time}${forTimeCompany}`, severity: data.severity, alert: data.alert, status: data.status ? 1 : 0, quota_type: Number(data.quota_type), }; } setLoading(true); fetchPost(apiRequest.ruleCenter.addQuota, { body: { env_id: 1, ...queryData, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { message.success(`添加操作下发成功`); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setAddMoadlVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; // 测试promsql规则 function fetchTestData(str) { setLoading(true); fetchPost(apiRequest.ruleCenter.testPromSql, { body: { expr: str, }, }) .then((res) => { handleResponse(res, (res) => { setTestQueryResults( res.data.map((e) => { let name = e.metric.__name__; delete e.metric.__name__; let str = JSON.stringify(e.metric).replace(/:/, "="); return { metric: `${name}${str}`, value: e.value[1], }; }) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } // 修改 const uploadQuota = (data) => { let queryData = {}; if (data.quota_type === "0") { let builtins_quota = data.builtins_quota; let result = dictionaries.current[builtins_quota[0]].filter( (f) => f.name == builtins_quota[1] )[0]; if (row.name == "数据分区使用率") { result.expr = row.expr; } queryData = { threshold_value: data.threshold_value, compare_str: data.compare_str, for_time: `${data.for_time}${forTimeCompany}`, severity: data.severity, alert: data.alert, // status: data.status ? 1 : 0, status: row.status, quota_type: Number(data.quota_type), builtins_quota: { description: result?.description, name: result?.name, expr: result?.expr, service: result?.service, }, }; } else if (data.quota_type === "1") { queryData = { summary: data.summary, description: data.description, expr: data.expr, service: data.service, threshold_value: data.threshold_value, compare_str: data.compare_str, for_time: `${data.for_time}${forTimeCompany}`, severity: data.severity, alert: data.alert, status: data.status ? 1 : 0, quota_type: Number(data.quota_type), }; } setLoading(true); fetchPost(apiRequest.ruleCenter.addQuota, { body: { env_id: 1, ...queryData, id: row.id, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { message.success(`修改操作下发成功`); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setUpDateVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; // 删除接口 const deleteQuota = (data) => { setLoading(true); fetchDelete(apiRequest.ruleCenter.deleteQuota, { params: { id: data.id, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { message.success(`删除操作下发成功`); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setDeleteMoadlVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; // 停用或者启用操作 const statusUpdate = (ids, status) => { setLoading(true); fetchPost(apiRequest.ruleCenter.batchUpdateRule, { body: { ids, status, }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (status == 1) { message.success(`启用操作下发成功`); } else { message.success(`停用操作下发成功`); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); // 批量停用弹框控制器 setStopVisible(false); // 单独停用弹框控制器 setStopRowVisible(false); // 批量启用弹框控制器 setStartVisible(false); // 单独启用弹框控制器 setStartRowVisible(false); fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, } ); }); }; useEffect(() => { fetchData(pagination); }, []); return (
setStartVisible(true)} disabled={checkedList.map((item) => item.id).length == 0} > 启用规则 { setStopVisible(true); }} > 停用规则 } placement="bottomCenter" >
规则名称: { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, alert: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
添加指标规则 } form={modalForm} onFinish={(data) => { addQuota(data); }} initialValues={{ compare_str: ">=", threshold_value: 30, quota_type: "0", for_time: 60, severity: "warning", status: true, }} >
{" "} { setRuleType(e.target.value); }} > 内置指标 自定义PromSQL {" "} {ruleType === "0" && ( <> {" "} { // if (value == 0) { // return Promise.reject(`只支持大于等于0的数字`); // } else { // return Promise.resolve("success"); // } // }, // }, ]} > {" "} { if (value == 0) { return Promise.reject(`只支持大于0的数字`); } else { return Promise.resolve("success"); } }, }, ]} > { setForTimeCompany(e); }} > } /> {" "} 警告 严重 )} {ruleType === "1" && ( <> {" "} { // if (value == 0) { // return Promise.reject(`只支持大于等于0的数字`); // } else { // return Promise.resolve("success"); // } // }, // }, ]} > {" "} { if (value == 0) { return Promise.reject(`只支持大于0的数字`); } else { return Promise.resolve("success"); } }, }, ]} > { setForTimeCompany(e); }} > } /> {" "} {" "} 警告 严重 {" "} {" "} )}
{/* 删除操作 */} 提示 } loading={loading} onFinish={() => { deleteQuota(row); }} >
确定要对 {row.alert} 规则{" "} 下发删除命令
PromSQL查询结果} loading={loading} >
({ ...i, key: idx }))} /> {/* 修改规则 */} 修改指标规则 } form={upDateForm} onFinish={(data) => { uploadQuota(data); // console.log(upDateForm.getFieldValue()); }} initialValues={{ compare_str: ">=", threshold_value: 30, quota_type: "0", for_time: 60, severity: "warning", status: true, }} >
{" "} console.log(row)} /> { setRuleType(e.target.value); }} > 内置指标 自定义PromSQL {" "} {ruleType === "0" && ( <> {" "} { // if (value == 0) { // return Promise.reject(`只支持大于等于0的数字`); // } else { // return Promise.resolve("success"); // } // }, // }, ]} > {" "} { if (value == 0) { return Promise.reject(`只支持大于0的数字`); } else { return Promise.resolve("success"); } }, }, ]} > { setForTimeCompany(e); }} > } /> {" "} 警告 严重 {/* */} )} {ruleType === "1" && ( <> {" "} { // if (value == 0) { // return Promise.reject(`只支持大于等于0的数字`); // } else { // return Promise.resolve("success"); // } // }, // }, ]} > {" "} { if (value == 0) { return Promise.reject(`只支持大于0的数字`); } else { return Promise.resolve("success"); } }, }, ]} > { setForTimeCompany(e); }} > } /> {" "} {" "} 警告 严重 {" "} {" "} )}
{/* 批量停用操作 */} 提示 } loading={loading} onFinish={() => { // deleteQuota(row); statusUpdate( checkedList.map((i) => i.id), 0 ); setCheckedList([]); }} >
确定要对 {checkedList.length}{" "} 条规则 下发停用命令
{/* 单独停用操作 */} 提示 } loading={loading} onFinish={() => { statusUpdate([row.id], 0); }} >
确定要对 {row.alert} 规则{" "} 下发停用命令
{/* 批量启用操作 */} 提示 } loading={loading} onFinish={() => { statusUpdate( checkedList.map((i) => i.id), 1 ); setCheckedList([]); // deleteQuota(row); }} >
确定要对 {checkedList.length}{" "} 条规则 下发启用命令
{/* 单独启用操作 */} 提示 } loading={loading} onFinish={() => { statusUpdate([row.id], 1); // deleteQuota(row); }} >
确定要对 {row.alert} 规则{" "} 下发启用命令
); }; export default RuleIndicator; ================================================ FILE: omp_web/src/pages/SelfHealingRecord/config/columns.js ================================================ import { renderDisc } from "@/utils/utils"; import { Tooltip, Badge } from "antd"; import moment from "moment"; const renderStatus = (state) => { switch (state) { case 1: return ( {renderDisc("normal", 7, -1)} 自愈成功 ); break; case 0: return ( {renderDisc("critical", 7, -1)} 自愈失败 ); break; case 2: return ( {renderDisc("warning", 7, -1)} 自愈中 ); break; default: return "-"; break; } }; const getColumnsConfig = ( queryRequest, setShowIframe, updateAlertRead, history ) => { return [ { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", width: 200, ellipsis: true, fixed: "left", sorter: (a, b) => a.instance_name - b.instance_name, sortDirections: ["descend", "ascend"], render: (text, record) => { return ( {record.instance_name ? record.instance_name : "-"} ); }, }, { title: "IP地址", key: "host_ip", width: 140, dataIndex: "host_ip", ellipsis: true, sorter: (a, b) => a.host_ip - b.host_ip, sortDirections: ["descend", "ascend"], align: "center", }, { title: "自愈状态", key: "state", dataIndex: "state", align: "center", width: 120, // sorter: (a, b) => a.severity - b.severity, // sortDirections: ["descend", "ascend"], //ellipsis: true, //width:120, usefilter: true, queryRequest: queryRequest, filterMenuList: [ { value: "1", text: "自愈成功", }, { value: "0", text: "自愈失败", }, { value: "2", text: "自愈中", }, ], render: renderStatus, }, { title: "重试次数", key: "healing_count", dataIndex: "healing_count", align: "center", //ellipsis: true, width: 80, render: (text) => { return text ? `${text}次` : "-"; }, }, { title: "故障时间", width: 180, key: "alert_time", dataIndex: "alert_time", align: "center", //ellipsis: true, // sorter: (a, b) => a.alert_time - b.alert_time, // sortDirections: ["descend", "ascend"], render: (text) => { if (text) { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; } return "-"; }, }, { title: "结束时间", width: 180, key: "end_time", dataIndex: "end_time", align: "center", //ellipsis: true, // sorter: (a, b) => a.create_time - b.create_time, // sortDirections: ["descend", "ascend"], render: (text) => { if (text) { let str = moment(text).format("YYYY-MM-DD HH:mm:ss"); return str; } return "-"; }, }, { title: "故障描述", key: "alert_content", dataIndex: "alert_content", align: "center", width: 300, ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "自愈日志", key: "healing_log", dataIndex: "healing_log", align: "center", width: 220, ellipsis: true, render: (text) => { return ( {text ? text : "-"} ); }, }, { title: "操作", width: 140, key: "", dataIndex: "", fixed: "right", align: "center", render: function renderFunc(text, record, index) { //console.log(record); return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/SelfHealingRecord/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpSelect, OmpDatePicker, OmpDrawer, } from "@/components"; import { Button, Select, message, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import getColumnsConfig from "./config/columns"; import { SearchOutlined } from "@ant-design/icons"; import moment from "moment"; import { useHistory, useLocation } from "react-router-dom"; const SelfHealingRecord = () => { const history = useHistory(); const location = useLocation(); const initIp = location.state?.ip; const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //选中的数据 const [checkedList, setCheckedList] = useState([]); //table表格数据 const [dataSource, setDataSource] = useState([]); const [ipListSource, setIpListSource] = useState([]); const [selectValue, setSelectValue] = useState(initIp); const [instanceSelectValue, setInstanceSelectValue] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 筛选label const [labelControl, setLabelControl] = useState( initIp ? "ip" : "instance_name" ); const [showIframe, setShowIframe] = useState({}); function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams = {}, ordering ) { setLoading(true); fetchGet(apiRequest.faultSelfHealing.querySelfHealingList, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchIPlist(); //fetchNameList(); }); } const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; const updateAlertRead = (ids = []) => { setLoading(true); fetchPost(apiRequest.faultSelfHealing.selfHeadlingIsRead, { body: { ids: ids, is_read: 1, }, }) .then((res) => { handleResponse(res, (res) => { message.success("已读成功"); }); }) .catch((e) => console.log(e)) .finally(() => { setCheckedList([]); setLoading(false); fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ...pagination.searchParams }, pagination.ordering ); }); }; useEffect(() => { fetchData(pagination, { alert_host_ip: location.state?.ip }); }, []); return (
{ if (!e) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, query_start_time: null, query_end_time: null, }, pagination.ordering ); } else { let result = e.filter((item) => item); if (result.length == 2) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, query_start_time: moment(e[0]).format( "YYYY-MM-DD HH:mm:ss" ), query_end_time: moment(e[1]).format( "YYYY-MM-DD HH:mm:ss" ), }, pagination.ordering ); } } }} />
{labelControl === "ip" && ( { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, host_ip: value }, pagination.ordering ); }} pagination={pagination} /> )} {labelControl === "instance_name" && ( { setInstanceSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, instance_name: null, }, pagination.ordering ); } }} onBlur={() => { if (instanceSelectValue) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, instance_name: instanceSelectValue, }, pagination.ordering ); } }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, instance_name: instanceSelectValue, }, pagination.ordering ); }} suffix={ !instanceSelectValue && ( ) } /> )}
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig( (params) => { // console.log(pagination.searchParams) fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, ...params }, pagination.ordering ); }, setShowIframe, updateAlertRead, history )} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
); }; export default SelfHealingRecord; ================================================ FILE: omp_web/src/pages/SelfHealingStrategy/StrategyModal.js ================================================ import { Button, Modal, Divider, Form, InputNumber, Spin, Switch, Select, } from "antd"; import { PlusSquareOutlined, FormOutlined } from "@ant-design/icons"; export const AddStrategyModal = ({ strategyModalType, addStrategy, updateStrategy, loading, modalForm, addModalVisibility, setAddModalVisibility, canHealingIns, strategyFormInit, keyArr, setKeyArr, }) => { return ( { setAddModalVisibility(false); setKeyArr([]); modalForm.setFieldsValue(strategyFormInit); }} visible={addModalVisibility} title={ {strategyModalType === "add" ? ( ) : ( )} {strategyModalType === "add" ? "添加自愈策略" : "编辑自愈策略"} } zIndex={1004} footer={null} destroyOnClose >
{ if (strategyModalType === "add") { addStrategy(data); } else { updateStrategy(data); } }} form={modalForm} initialValues={strategyFormInit} > {/* */}
); }; ================================================ FILE: omp_web/src/pages/SelfHealingStrategy/config/columns.js ================================================ import { renderDisc } from "@/utils/utils"; import { Tooltip } from "antd"; const getColumnsConfig = ( setStrategyRow, setDeleteStrategyModal, setStrategyModalType, setStrategyModalVisibility, strategyForm, queryCanHealing ) => { return [ { title: "序号", key: "_idx", dataIndex: "_idx", align: "center", width: 40, fixed: "left", }, { title: "自愈实例", key: "repair_instance", dataIndex: "repair_instance", align: "center", width: 200, ellipsis: true, render: (text) => { // if (text.length > 0 && text[0] === "all") return "所有服务"; const textMap = { host: "主机监控Agent", component: "基础组件", service: "自研服务", }; const resText = text.map((e) => textMap[e]); return ( {resText.join(", ")} ); }, }, { title: "自愈类型", key: "instance_tp", dataIndex: "instance_tp", align: "center", width: 60, ellipsis: true, render: (text) => { if (text === 0) { return "启动 [start]"; } else { return "重启 [restart]"; } }, }, { title: "探测周期", key: "fresh_rate", dataIndex: "fresh_rate", align: "center", width: 100, ellipsis: true, render: (text) => { return `${text} min`; }, }, { title: "重试次数", key: "max_healing_count", dataIndex: "max_healing_count", align: "center", width: 100, ellipsis: true, }, { title: "是否生效", key: "used", dataIndex: "used", align: "center", width: 60, ellipsis: true, render: (text) => { if (text) { return {renderDisc("normal", 7, -1)}是; } else { return {renderDisc("critical", 7, -1)}否; } }, }, { title: "操作", width: 100, key: "", dataIndex: "", align: "center", fixed: "right", render: (text, record, index) => { return ( ); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/SelfHealingStrategy/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal } from "@/components"; import { Form, Button, message } from "antd"; import { useState, useEffect } from "react"; import { handleResponse } from "@/utils/utils"; import { fetchGet, fetchDelete, fetchPost, fetchPut } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { ExclamationCircleOutlined } from "@ant-design/icons"; import getColumnsConfig from "./config/columns"; import { AddStrategyModal } from "./StrategyModal"; import { useHistory, useLocation } from "react-router-dom"; const BackupStrategy = () => { const location = useLocation(); const history = useHistory(); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); // 自定义参数 // 增/改自愈策略共用 const [strategyRow, setStrategyRow] = useState({}); const [strategyModalType, setStrategyModalType] = useState("add"); const [strategyModalVisibility, setStrategyModalVisibility] = useState(false); const [strategyLoading, setStrategyLoading] = useState(false); const [keyArr, setKeyArr] = useState([]); // 自愈策略表单 const [strategyForm] = Form.useForm(); // 自愈组件全量数据 const [canHealingIns, setcanHealingIns] = useState([]); // 删除策略 const [deleteStrategyModal, setDeleteStrategyModal] = useState(false); // 策略表单初始值 const strategyFormInit = { repair_instance: [], fresh_rate: 30, max_healing_count: 5, instance_tp: 0, used: false, }; // 策略列表查询 const fetchData = () => { setLoading(true); fetchGet(apiRequest.faultSelfHealing.selfHealingStrategy) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.map((item, idx) => { return { ...item, _idx: idx + 1, }; }) ); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; // 查询可自愈实例 const queryCanHealing = () => { setStrategyLoading(true); fetchGet(apiRequest.faultSelfHealing.selfHealingStrategy, { params: { instance: true, }, }) .then((res) => { handleResponse(res, () => { setcanHealingIns(res.data.data); }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 添加自愈策略 const addStrategy = (data) => { setStrategyLoading(true); fetchPost(apiRequest.faultSelfHealing.selfHealingStrategy, { body: data, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("添加自愈策略成功"); strategyForm.setFieldsValue(strategyFormInit); fetchData(); setKeyArr([]); setStrategyModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 编辑自愈策略 const updateStrategy = (data) => { setStrategyLoading(true); fetchPut( `${apiRequest.faultSelfHealing.selfHealingStrategy}${strategyRow.id}/`, { body: data, } ) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("修改自愈策略成功"); strategyForm.setFieldsValue(strategyFormInit); fetchData(); setKeyArr([]); setStrategyModalVisibility(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setStrategyLoading(false); }); }; // 删除自愈策略 const deleteStrategy = () => { setLoading(true); fetchDelete( `${apiRequest.faultSelfHealing.selfHealingStrategy}${strategyRow.id}/` ) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { message.success("删除成功"); fetchData(); setDeleteStrategyModal(false); } else { message.warning(res.message); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchData(); }, []); return (
record.id} noScroll={true} />
提示 } loading={loading} onFinish={() => { deleteStrategy(); }} >
确定 删除 该策略吗?
); }; export default BackupStrategy; ================================================ FILE: omp_web/src/pages/SelfHealingStrategy/index.module.less ================================================ .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .content { padding-left: 40px; padding-top: 30px; display: flex; align-items: center; .label { padding-right: 30px; } } .tips { margin-top: 30px; padding-left: 40px; font-size: 13px; } .saveButtonWrapper { height: 50px; display: flex; align-items: center; justify-content: center; } .saveButton { margin-left: 50%; transform: translateX(-50%); } ================================================ FILE: omp_web/src/pages/ServiceManagement/config/columns.js ================================================ import { nonEmptyProcessing, renderDisc, RenderStatusForResult, } from "@/utils/utils"; import { OmpToolTip } from "@/components"; import { DesktopOutlined } from "@ant-design/icons"; import { Dropdown, Menu, Drawer, Tooltip, Spin, Timeline } from "antd"; import moment from "moment"; import styles from "../index.module.less"; import { useSelector } from "react-redux"; import { useRef } from "react"; const colorConfig = { normal: null, warning: "#ffbf00", critical: "#f04134", }; export const DetailHost = ({ isShowDrawer, setIsShowDrawer, loading, data, setInstallationRecordModal, queryServiceInstallHistoryDetail, }) => { // 视口宽度 const viewHeight = useSelector((state) => state.layouts.viewSize.height); const wrapperRef = useRef(null); return ( 服务信息面板 实例名称: {isShowDrawer.record?.service_instance_name} } headerStyle={{ padding: "19px 24px", }} placement="right" closable={true} width={`calc(100% - 200px)`} style={{ height: "calc(100%)", // paddingTop: "60px", }} onClose={() => { setIsShowDrawer({ ...isShowDrawer, isOpen: false, }); }} visible={isShowDrawer.isOpen} bodyStyle={{ padding: 10, //paddingLeft:10, backgroundColor: "#e7e9f0", //"#f4f6f8" height: "calc(100%)", }} destroyOnClose={true} >
基本信息
实例名称
{isShowDrawer.record?.service_instance_name}
服务名称
{nonEmptyProcessing(isShowDrawer.record?.app_name)}
{/*
IP地址
{isShowDrawer.record.ip}
*/}
版本
{isShowDrawer.record?.app_version}
服务分类
{isShowDrawer.record?.label_name}
集群模式
{isShowDrawer.record?.cluster_type}
IP地址
{isShowDrawer.record?.ip}
安装目录
{data.install_info?.base_dir}
数据目录
{data.install_info?.data_dir}
日志目录
{data.install_info?.log_dir}
端口号
{data.install_info?.service_port}
用户名
{data.install_info?.username}
密码
{data.install_info?.password}
安装时间
{data?.created ? moment(data?.created).format("YYYY-MM-DD HH:mm:ss") : "-"}
历史记录
{data.history?.map((item) => { return (

[ {item.username}] {item.description}

{moment(item.created).format("YYYY-MM-DD HH:mm:ss")}

); })}
); }; //操作 const renderMenu = ( // setUpdateMoadlVisible, // setCloseMaintainModal, // setOpenMaintainModal, record, setOperateAciton, setServiceAcitonModal, queryDeleteMsg, deleteConditionReset ) => { return ( { setOperateAciton(1); setServiceAcitonModal(true); }} > 启动 { setOperateAciton(2); setServiceAcitonModal(true); }} > 停止 { setOperateAciton(3); setServiceAcitonModal(true); }} > 重启 { queryDeleteMsg([record]); setOperateAciton(4); setServiceAcitonModal(true); deleteConditionReset(); }} > 卸载 ); }; const renderStatus = (text) => { switch (text) { case "未监控": return ( {renderDisc("notMonitored", 7, -1)} {text} ); case "启动中": return ( {renderDisc("warning", 7, -1)} {text} ); case "停止中": return ( {renderDisc("warning", 7, -1)} {text} ); case "重启中": return ( {renderDisc("warning", 7, -1)} {text} ); case "未知": return ( {renderDisc("warning", 7, -1)} {text} ); case "安装中": return ( {renderDisc("warning", 7, -1)} {text} ); case "待安装": return ( {renderDisc("warning", 7, -1)} {text} ); case "停止": return ( {renderDisc("critical", 7, -1)} {text} ); case "安装失败": return ( {renderDisc("critical", 7, -1)} {text} ); default: return ( {renderDisc("normal", 7, -1)} {text} ); } }; const getColumnsConfig = ( setIsShowDrawer, setRow, //setUpdateMoadlVisible, fetchHistoryData, // setCloseMaintainModal, // setOpenMaintainModal, //setShowIframe, history, labelsData, queryRequest, initfilterAppType, initfilterLabelName, setShowIframe, setOperateAciton, setServiceAcitonModal, queryDeleteMsg, // 删除的前置条件重置 deleteConditionReset ) => { return [ { title: "实例名称", key: "service_instance_name", dataIndex: "service_instance_name", sorter: (a, b) => a.service_instance_name - b.service_instance_name, sortDirections: ["descend", "ascend"], align: "center", ellipsis: true, fixed: "left", render: (text, record) => { return ( { fetchHistoryData(record.id); setIsShowDrawer({ isOpen: true, record: record, }); }} > {text ? text : "-"} ); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", sorter: (a, b) => a.ip - b.ip, sortDirections: ["descend", "ascend"], align: "center", //width: 140, render: (text, record) => { let str = nonEmptyProcessing(text); if (str == "-") { return "-"; } else { return {str}; } }, //ellipsis: true, }, { title: "CPU使用率", key: "cpu_usage", dataIndex: "cpu_usage", align: "center", sorter: (a, b) => a.cpu_usage - b.cpu_usage, sortDirections: ["descend", "ascend"], render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, }, { title: "内存使用率", key: "mem_usage", dataIndex: "mem_usage", sorter: (a, b) => a.mem_usage - b.mem_usage, sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { let str = nonEmptyProcessing(text); return str == "-" ? ( "-" ) : ( {str}% ); }, }, { title: "状态", key: "service_status", dataIndex: "service_status", align: "center", //ellipsis: true, render: (text) => { return renderStatus(text); }, }, { title: "告警次数", key: "alert_count", dataIndex: "alert_count", align: "center", render: (text, record) => { if (text == "-" || text == "0次") { return text; } else { return ( { text && history.push({ pathname: "/application-monitoring/alarm-log", state: { ip: record.ip, }, }); }} > {text} ); } }, //ellipsis: true, }, { title: "端口", key: "port", dataIndex: "port", align: "center", ellipsis: true, render: (text) => { return {text ? text : "-"}; }, }, { title: "服务名称", key: "app_name", dataIndex: "app_name", align: "center", ellipsis: true, }, { title: "版本", key: "app_version", dataIndex: "app_version", align: "center", ellipsis: true, }, { title: "功能模块", key: "label_name", dataIndex: "label_name", usefilter: true, queryRequest: queryRequest, ellipsis: true, initfilter: initfilterLabelName, filterMenuList: labelsData.map((item) => ({ value: item, text: item })), align: "center", render: (text) => { return {text ? text : "-"}; }, }, { title: "服务类型", key: "app_type", dataIndex: "app_type", align: "center", usefilter: true, queryRequest: queryRequest, initfilter: initfilterAppType, filterMenuList: [ { value: 0, text: "基础组件", }, { value: 1, text: "应用服务", }, ], render: (text) => { return text ? "应用服务" : "基础组件"; }, //ellipsis: true, }, { title: "集群模式", key: "cluster_type", dataIndex: "cluster_type", align: "center", //ellipsis: true, }, { title: "操作", //width: 100, width: 140, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return (
{ setRow(record); }} style={{ display: "flex", justifyContent: "space-around" }} >
); }, }, ]; }; export default getColumnsConfig; ================================================ FILE: omp_web/src/pages/ServiceManagement/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal, OmpSelect, OmpDrawer, } from "@/components"; import { Button, message, Menu, Dropdown, Input, Select, Checkbox } from "antd"; import { useState, useEffect, useRef } from "react"; import { handleResponse, _idxInit, refreshTime } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { useDispatch } from "react-redux"; import getColumnsConfig, { DetailHost } from "./config/columns"; import { DownOutlined, ExclamationCircleOutlined, SearchOutlined, } from "@ant-design/icons"; import { useHistory, useLocation } from "react-router-dom"; const ServiceManagement = () => { const location = useLocation(); const initIp = location.state?.ip; console.log(initIp); const history = useHistory(); const dispatch = useDispatch(); const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //选中的数据 const [checkedList, setCheckedList] = useState([]); //table表格数据 const [dataSource, setDataSource] = useState([]); const [ipListSource, setIpListSource] = useState([]); const [selectValue, setSelectValue] = useState(initIp); const [labelsData, setLabelsData] = useState([]); const [instanceSelectValue, setInstanceSelectValue] = useState(""); const [labelControl, setLabelControl] = useState( initIp ? "ip" : "instance_name" ); const [installationRecordModal, setInstallationRecordModal] = useState(false); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const [isShowDrawer, setIsShowDrawer] = useState({ isOpen: false, src: "", record: {}, }); const [showIframe, setShowIframe] = useState({}); // 定义row存数据 const [row, setRow] = useState({}); // 服务详情历史数据 const [historyData, setHistoryData] = useState([]); // 服务详情loading const [historyLoading, setHistoryLoading] = useState([]); //const [showIframe, setShowIframe] = useState({}); const [serviceAcitonModal, setServiceAcitonModal] = useState(false); const [currentSerAcitonModal, setCurrentSerAcitonModal] = useState(false); // 1启动,2停止,3重启,4删除 let operateObj = { 1: "启动", 2: "停止", 3: "重启", 4: "删除", }; const [operateAciton, setOperateAciton] = useState(1); // 删除操作的提示语 const [deleteMsg, setDeleteMsg] = useState(""); // 删除操作的再次确认 const [confirmDeletion, setConfirmDeletion] = useState(false); // 确认删除的维度 const [deleteDimension, setDeleteDimension] = useState(false); // 列表查询 function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.appStore.services, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); fetchIPlist(); fetchSearchlist(); }); } const fetchIPlist = () => { setSearchLoading(true); fetchGet(apiRequest.machineManagement.ipList) .then((res) => { handleResponse(res, (res) => { setIpListSource(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setSearchLoading(false); }); }; // 功能模块筛选 const fetchSearchlist = () => { //setSearchLoading(true); fetchGet(apiRequest.appStore.queryLabels) .then((res) => { handleResponse(res, (res) => { setLabelsData(res.data); //console.log(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { //setSearchLoading(false); }); }; const fetchHistoryData = (id) => { setHistoryLoading(true); fetchGet(`${apiRequest.appStore.servicesDetail}/${id}/`, { // params: { // id: id, // }, }) .then((res) => { handleResponse(res, (res) => { setHistoryData(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setHistoryLoading(false); }); }; const t = useRef(null); // 服务的启动|停止|重启 const operateService = (data, operate, del_file) => { setLoading(true); fetchPost(apiRequest.appStore.servicesAction, { body: { data: data.map((i) => ({ action: operate, id: i.id, del_file: del_file || null, operation_user: localStorage.getItem("username"), })), }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { message.success(`${operateObj[operateAciton]}操作下发成功`); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setServiceAcitonModal(false); setCurrentSerAcitonModal(false); // setRestartHostAgentModal(false); setCheckedList([]); setRow({}); setLoading(true); t.current = setTimeout(() => { fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ...pagination.searchParams, ip: selectValue, service_instance_name: instanceSelectValue, }, pagination.ordering ); }, 1500); }); }; const containerRef = useRef(null); const timer = useRef(null); const [log, setLog] = useState(""); const queryServiceInstallHistoryDetail = (id) => { fetchGet(apiRequest.appStore.serviceInstallHistoryDetail, { params: { id: id, }, }) .then((res) => { handleResponse(res, (res) => { setLog(res.data[0].log); if ( res.data[0].install_step_status == 1 || res.data[0].install_step_status == 0 ) { timer.current = setTimeout(() => { queryServiceInstallHistoryDetail(id); }, 2000); } }); }) .catch((e) => console.log(e)) .finally(() => { containerRef.current.scrollTop = containerRef.current.scrollHeight; }); }; // 删除操作的提示语获取 const queryDeleteMsg = (data) => { fetchPost(apiRequest.appStore.servicesDeleteMsg, { body: { data: data.map((i) => ({ id: i.id, action: "4", operation_user: localStorage.getItem("username"), })), }, }) .then((res) => { //console.log(operateObj[operateAciton]) handleResponse(res, (res) => { if (res && res.data) { let key = res.data?.split(":")[0]; let values = res.data?.split(":")[1]; let arr = values?.split(","); let dom = (
{key}
); setDeleteMsg(dom); } }); }) .catch((e) => console.log(e)) .finally(() => {}); }; useEffect(() => { fetchData( { current: pagination.current, pageSize: pagination.pageSize }, { ip: location.state?.ip, app_type: location.state?.app_type, label_name: location.state?.label_name, } ); return () => { if (t.current) { clearTimeout(t.current); } }; }, []); return (
{/* { setOperateAciton(1); setServiceAcitonModal(true); }} disabled={ checkedList.filter((e) => { return e.operable; }).length == 0 } > 启动 */} { return e.operable; }).length == 0 } onClick={() => { setOperateAciton(2); setServiceAcitonModal(true); }} > 停止 { return e.operable; }).length == 0 } onClick={() => { setOperateAciton(3); setServiceAcitonModal(true); }} > 重启 { queryDeleteMsg(checkedList); setOperateAciton(4); setServiceAcitonModal(true); setConfirmDeletion(true); setDeleteDimension(false); }} > 删除 } placement="bottomCenter" >
{labelControl === "ip" && ( { fetchData( { current: 1, pageSize: pagination.pageSize }, { ip: value }, pagination.ordering ); }} /> )} {labelControl === "instance_name" && ( { setInstanceSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, service_instance_name: null, }, pagination.ordering ); } }} onBlur={() => { if (instanceSelectValue) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, service_instance_name: instanceSelectValue, }, pagination.ordering ); } }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, service_instance_name: instanceSelectValue, }, pagination.ordering ); }} suffix={ !instanceSelectValue && ( ) } /> )}
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={getColumnsConfig( setIsShowDrawer, setRow, fetchHistoryData, history, labelsData, (params) => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, ...params }, pagination.ordering ); }, location.state?.app_type, location.state?.label_name, setShowIframe, setOperateAciton, setCurrentSerAcitonModal, queryDeleteMsg, () => { setConfirmDeletion(true); setDeleteDimension(false); } )} notSelectable={(record) => ({ // 部署中的不能选中 disabled: !( record.service_status === "正常" || record.service_status === "停止" || record.service_status === "未监控" ), })} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
queryServiceInstallHistoryDetail(id) } /> 提示 } loading={loading} onFinish={() => { let data = null; if (operateAciton === 4) { data = checkedList; } else { data = checkedList.filter((e) => { return e.operable; }); } operateService(data, operateAciton, deleteDimension); // fetchMaintainChange(false, [row]); }} >
确定要对{" "} {checkedList.length} 个 服务下发{" "} {operateObj[operateAciton]}{" "} 操作? {operateAciton == 4 && deleteMsg && ( <>
{deleteMsg}
{ setDeleteDimension(e.target.checked); }} > 同时卸载服务
{/*
{ setConfirmDeletion(!e.target.checked) }} >确认删除
*/} )}
提示 } loading={loading} onFinish={() => { operateService([row], operateAciton, deleteDimension); }} >
确定要对 当前 服务下发{" "} {operateObj[operateAciton]}{" "} 操作? {operateAciton == 4 && deleteMsg && ( <>
{deleteMsg}
{ setDeleteDimension(e.target.checked); }} > 同时卸载服务
{/*
{ setConfirmDeletion(!e.target.checked) }} >确认删除
*/} )}
{ if (timer.current) { clearTimeout(timer.current); } }} noFooter={true} visibleHandle={[installationRecordModal, setInstallationRecordModal]} >
{log ? log : "正在安装..."}
); }; const ExpandCollapseMsg = ({ length, all }) => { const [isOpen, setIsOpen] = useState(false); if (!all) { return <>; } if (isOpen) { return ( <> {all.map((item) => { return
{item}
; })} setIsOpen(false)}>收起 ); } else { return ( <> {all?.slice(0, length).map((item) => { return
{item}
; })} {all.length > length && setIsOpen(true)}>...展开} ); } }; export default ServiceManagement; ================================================ FILE: omp_web/src/pages/ServiceManagement/index.module.less ================================================ .serviceManagement { display: flex; } .subMenu { width: 160px; } .warningSearch { display: flex; margin-top: 10px; margin-bottom: 10px; & > div:nth-child(1) { margin-right: 10px; } & > div:last-child { margin-left: auto; } } .antdTableExpandedRow { margin: 0; padding: 0; height: 20px; display: flex; span { margin-left: 60px; } } .redType { background-color: #ff4d4f; color: #fff; border-color:#ff4d4f; } .formItem { margin-bottom: 15px; display: flex; justify-content: center; align-items: center; & > span:nth-child(1) { display: inline-block; width: 100px; font-size: 14px; //font-weight: 500; color: #333; text-align: right; } & > input:nth-child(2) { width: 240px; } } .serviceTable { //cursor: url('../../public/conf/logo.svg'),default; cursor: pointer; } .omp_spin_wrapper{ height: calc(100%); } :global { .ant-dropdown-menu-item:hover, .ant-dropdown-menu-submenu-title:hover { background-color:#e6f1f6; color:#2e7cee } //悬停样式会覆盖disable样式,在这里把disable权限提高 .ant-dropdown-menu-item-disabled { background-color: #fff!important; color:rgba(0, 0, 0, 0.25)!important } } ================================================ FILE: omp_web/src/pages/SystemLog/index.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { Button, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { SearchOutlined } from "@ant-design/icons"; const SystemLog = () => { const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [userListSource, setUserListSource] = useState([]); const [searchValue, setSearchValue] = useState(""); const [selectValue, setSelectValue] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "序号", width: 40, key: "_idx", dataIndex: "_idx", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, fixed: "left", }, { title: "用户名", key: "username", width: 100, dataIndex: "username", sorter: (a, b) => a.username - b.username, sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "IP地址", key: "request_ip", dataIndex: "request_ip", sorter: (a, b) => a.request_ip - b.request_ip, sortDirections: ["descend", "ascend"], align: "center", width: 100, render: nonEmptyProcessing, // render: (text) => { // if (text) { // return "正常"; // } else { // return "停用"; // } // }, }, { title: "操作类型", key: "request_method", width: 100, dataIndex: "request_method", // sorter: (a, b) => a.request_method - b.request_method, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "描述", key: "description", width: 100, dataIndex: "description", // sorter: (a, b) => a.description - b.description, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "创建时间", key: "create_time", dataIndex: "create_time", align: "center", width: 100, sorter: (a, b) => a.create_time - b.create_time, sortDirections: ["descend", "ascend"], render: nonEmptyProcessing, // render: (text) => { // if (text) { // return moment(text).format("YYYY-MM-DD HH:mm:ss"); // } else { // return "-"; // } // }, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.operationRecord.querySystemLog, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { if (!searchParams) { setUserListSource(res.data.results.map((item) => item.username)); } setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
用户名: {/* { setSelectValue(e) console.log(e) fetchData( { current: pagination.current, pageSize: pagination.pageSize }, {username:e}, pagination.ordering ); }} style={{ width: 200 }} /> */} { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default SystemLog; ================================================ FILE: omp_web/src/pages/SystemManagement/index.js ================================================ import { OmpContentWrapper, OmpMessageModal, } from "@/components"; import { message, Switch, } from "antd"; import { useState } from "react"; import { handleResponse, _idxInit, } from "@/utils/utils"; import { fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import styles from "./index.module.less"; import { ToolFilled, ExclamationCircleOutlined, } from "@ant-design/icons"; import { getMaintenanceChangeAction } from "./store/actionsCreators"; import { useSelector, useDispatch } from "react-redux"; const SystemManagement = () => { const [loading, setLoading] = useState(false); const dispatch = useDispatch(); //是否展示维护模式提示词 const isMaintenance = useSelector( (state) => state.systemManagement.isMaintenance ); const [closeMaintenanceModal, setCloseMaintenanceModal] = useState(false); const [openMaintenanceModal, setOpenMaintenanceModal] = useState(false); // 更改维护模式 const changeMaintain = (e)=>{ setLoading(true); fetchPost(apiRequest.environment.queryMaintainState, { body: { matcher_name:"env", matcher_value:"default" }, }) .then((res) => { handleResponse(res, (res) => { if(res.code == 0){ if (e) { message.success("已进入全局维护模式") dispatch(getMaintenanceChangeAction(true)); } else { message.success("已退出全局维护模式") dispatch(getMaintenanceChangeAction(false)); } } if(res.code == 1){ message.warning(res.message) } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); setOpenMaintenanceModal(false); setCloseMaintenanceModal(false); }); } return (
维护模式
启用: { if (e) { setOpenMaintenanceModal(true); } else { setCloseMaintenanceModal(true); } }} />

开启维护模式后,将暂停平台异常告警功能;此功能适用于计划性升级、变更操作期间,避免造成误报带来的影响。

提示 } loading={loading} onFinish={() => { changeMaintain(true) }} >
确定进入全局维护模式 ?
提示 } loading={loading} onFinish={() => { changeMaintain(false) }} >
确定退出全局维护模式 ?
); }; export default SystemManagement; ================================================ FILE: omp_web/src/pages/SystemManagement/index.module.less ================================================ .header { padding: 10px; padding-left: 20px; background-color: #f7f7f7; } .content { padding-left: 40px; padding-top: 30px; display: flex; align-items: center; .label { padding-right: 30px; } } .tips { margin-top: 30px; padding-left: 40px; font-size: 13px; } ================================================ FILE: omp_web/src/pages/SystemManagement/store/actionsCreators.js ================================================ import * as actionTypes from "./constants"; export const getMaintenanceChangeAction = (value) => ({ type: actionTypes.CHANGE_MAINTENANCE, payload: { isMaintenance:value } }); ================================================ FILE: omp_web/src/pages/SystemManagement/store/constants.js ================================================ export const CHANGE_MAINTENANCE = "CHANGE_MAINTENANCE"; ================================================ FILE: omp_web/src/pages/SystemManagement/store/index.js ================================================ import reducer from "./reduer"; export { reducer }; ================================================ FILE: omp_web/src/pages/SystemManagement/store/reduer.js ================================================ import * as actionTypes from "./constants"; const defaultState = { isMaintenance:false }; function reducer(state = defaultState,action){ switch(action.type){ case actionTypes.CHANGE_MAINTENANCE: return {...state, isMaintenance: action.payload.isMaintenance}; default: return state; } } export default reducer; ================================================ FILE: omp_web/src/pages/TaskRecord/index.js ================================================ import { OmpContentWrapper, OmpTable } from "@/components"; import { Button, Input } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, nonEmptyProcessing, renderDisc, } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import moment from "moment"; import { SearchOutlined } from "@ant-design/icons"; import { useHistory } from "react-router-dom"; const kindMap = ["管理工具", "检查工具", "安全工具", "其他工具"]; const TaskRecord = () => { const [loading, setLoading] = useState(false); const history = useHistory(); //table表格数据 const [dataSource, setDataSource] = useState([]); const [selectValue, setSelectValue] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); const columns = [ { title: "任务标题", width: 100, key: "task_name", dataIndex: "task_name", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", fixed: "left", render: (text, record) => { if (!text) { return "-"; } return ( { history.push( `/utilitie/tool-management/tool-execution-results/${record.id}` ); }} > {text} ); }, }, { title: "分类", key: "kind", width: 100, dataIndex: "kind", align: "center", usefilter: true, queryRequest: (params) => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, ...params }, pagination.ordering ); }, filterMenuList: [ { value: 0, text: "管理工具", }, { value: 1, text: "检查工具", }, { value: 2, text: "安全工具", }, { value: 3, text: "其他工具", }, ], render: (text) => { return kindMap[text]; }, }, { title: "执行时间", key: "start_time", dataIndex: "start_time", width: 100, sorter: (a, b) => a.start_time - b.start_time, sortDirections: ["descend", "ascend"], align: "center", render: (text) => { return text ? moment(text).format("YYYY-MM-DD HH:mm:ss") : "-"; }, }, { title: "状态", key: "status", dataIndex: "status", width: 100, align: "center", render: (text, record, index) => { if (!text && text !== 0) { return "-"; } else if (text === 0) { return
{renderDisc("warning", 7, -1)}待执行
; } else if (text === 1) { return
{renderDisc("warning", 7, -1)}执行中
; } else if (text === 2) { return
{renderDisc("normal", 7, -1)}执行成功
; } else if (text === 3) { return
{renderDisc("critical", 7, -1)}执行失败
; } else { return text; } }, }, { title: "执行用时", key: "duration", dataIndex: "duration", align: "center", width: 100, render: nonEmptyProcessing, }, { title: "操作", width: 60, key: "", dataIndex: "", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return ( ); }, }, ]; function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.utilitie.queryHistory, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } useEffect(() => { fetchData(pagination); }, []); return (
名称: { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, search: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, search: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, search: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
); }; export default TaskRecord; ================================================ FILE: omp_web/src/pages/ToolExecution/index.js ================================================ import { useEffect, useState } from "react"; import { Button, Form, Spin, Input, InputNumber, Tooltip, Checkbox, Modal, Select, Upload, message, } from "antd"; import { useHistory, useLocation } from "react-router-dom"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { OmpContentWrapper, OmpTable } from "@/components"; import styles from "./index.module.less"; import { handleResponse, _idxInit } from "@/utils/utils"; import { QuestionCircleOutlined, CloseOutlined, UploadOutlined, } from "@ant-design/icons"; import star from "./asterisk.svg"; const ToolExecution = () => { const history = useHistory(); const locationArr = useLocation().pathname.split("/"); const [loading, setLoading] = useState(false); const [executionLoading, setExecutionLoading] = useState(false); const [form] = Form.useForm(); const [conf, setConf] = useState(); // 是否采用纳管用户 const [isUseManagement, setIsUseManagement] = useState(true); // 执行对象弹框控制器 const [executionTarget, setExecutionTarget] = useState(false); // 选中的数据 const [checkedList, setCheckedList] = useState([]); // 执行对象数据 const [executionData, setExecutionData] = useState([]); // 是否展示执行对象校验信息 const [isShowErrMsg, setIsShowErrMsg] = useState(false); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); // 执行对象列表 const [executionList, setExecutionList] = useState([]); const initColumns = [ { title: "实例名称", key: "instance_name", dataIndex: "instance_name", align: "center", ellipsis: true, width: 150, fixed: "left", render: (text, record) => { return (
{text}
); }, }, { title: "IP地址", key: "ip", dataIndex: "ip", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, { title: "Agent状态", key: "host_agent_state", dataIndex: "host_agent_state", align: "center", ellipsis: true, width: 120, render: (text, record) => { let v = text || "-"; return (
{v}
); }, }, ]; // 执行对象columns const [executionColumns, setExecutionColumn] = useState(initColumns); // 扩展 const [extendForm, setExtendForm] = useState([]); const queryConf = () => { setLoading(true); fetchGet( `${apiRequest.utilitie.queryFormConf}${ locationArr[locationArr.length - 1] }/` ) .then((res) => { handleResponse(res, (res) => { setConf(res.data); if (!res.data.default_form.runuser) { setIsUseManagement(true); } else { setIsUseManagement(false); } // 设置扩展表单组件默认值 if (res.data?.script_args) { let script_args = res.data?.script_args; setExtendForm(script_args); script_args.forEach((item) => { console.log(item); form.setFieldsValue({ [item.key]: item.default, }); }); } // 设置固定表单默认值 form.setFieldsValue({ task_name: res?.data?.default_form?.task_name, timeout: res?.data?.default_form?.timeout, runuser: res?.data?.default_form?.runuser, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; const queryExecutionList = (pageParams = { current: 1, pageSize: 10 }) => { setExecutionLoading(true); fetchGet( `${apiRequest.utilitie.queryFormConf}${ locationArr[locationArr.length - 1] }/target-object`, { params: { page: pageParams.current, size: pageParams.pageSize, }, } ) .then((res) => { handleResponse(res, (res) => { // 当有需要扩展的column时 // setExecutionColumn if ( res.data.results && res.data.results[0] && res.data.results[0].modifiable_kwargs ) { let extendItems = []; let modifiableKwargs = res.data.results[0].modifiable_kwargs; for (const key in modifiableKwargs) { extendItems.push({ title: key, key: key, dataIndex: key, align: "center", ellipsis: true, width: 150, render: (text, record) => { return (
{text}
); }, }); } setExecutionColumn([...initColumns, ...extendItems]); } setExecutionList( res.data.results.map((m, idx) => { return { ...m, ...m?.modifiable_kwargs, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, }); }); }) .catch((e) => console.log(e)) .finally(() => { setExecutionTarget(true); setExecutionLoading(false); }); }; const getExtendFormComponent = (item) => { switch (item.type) { case "input": return ( ); break; case "select": return ( ); break; case "file": return ( { const fileSize = file.size / 1024 / 1024; //单位是mb if (Math.ceil(fileSize) > 20) { message.error("仅支持传入20MB以内文件"); return Upload.LIST_IGNORE; } // return Upload.LIST_IGNORE; }} > ); break; default: return "暂无类型"; break; } }; // 执行任务下发 const performTasks = () => { setLoading(true); let formData = form.getFieldsValue(); let defaultForm = { ...conf.default_form }; let scriptArgs = [...conf.script_args]; // defaultForm填充数据 for (const key in defaultForm) { defaultForm[key] = formData[key]; } defaultForm.target_objs = executionData; // scriptArgs数据填充 scriptArgs = scriptArgs.map((item) => { if (item.type == "file") { let defaultData = {}; // 为了可读性,分两层写 console.log(formData); if ( formData[item.key] && formData[item.key].file && formData[item.key].file.status == "done" ) { if (formData[item.key].file.response.code == 0) { defaultData.file_name = formData[item.key].file.response.data.file_name; defaultData.file_url = formData[item.key].file.response.data.file_url; defaultData.union_id = formData[item.key].file.response.data.union_id; } } return { ...item, default: defaultData, }; } return { ...item, default: formData[item.key], }; }); fetchPost( `${apiRequest.utilitie.queryFormConf}${ locationArr[locationArr.length - 1] }/answer`, { body: { default_form: defaultForm, script_args: scriptArgs, }, } ) .then((res) => { if (res && res.data) { if (res.data.code == 1) { message.warning(res.data.message); } if (res.data.code == 0) { message.success("执行命令下发成功"); setTimeout(() => { history.push( `/utilitie/tool-management/tool-execution-results/${res.data.data.id}` ); }, 300); } } }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { queryConf(); }, []); return (
创建任务: {conf?.name}
{ if (executionData.length !== 0) { console.log("通过校验了"); performTasks(); } }} form={form} > 请选择执行对象
} label={ 执行对象 } >
{executionData.map((i) => ( ))}
{ console.log("执行了"); let reg = new RegExp(/^[1-9]\d*$/, "g"); if (value == 0) { return Promise.reject("请输入正整数"); } if (value) { if (!reg.test(value)) { return Promise.reject("请输入正整数"); } return Promise.resolve("success"); } else { return Promise.resolve("success"); } }, }, ]} > {" "} { form.setFieldsValue({ runuser: null, }); setIsUseManagement(e.target.checked); }} checked={isUseManagement} > 使用纳管用户 {extendForm.map((item) => { return getExtendFormComponent(item); })}
{ setCheckedList([]); }} onCancel={() => { setExecutionTarget(false); }} visible={executionTarget} footer={null} //width={1000} loading={executionLoading} bodyStyle={{ paddingLeft: 30, paddingRight: 30, }} destroyOnClose >
{ setTimeout(() => { queryExecutionList(e); }, 200); }} dataSource={executionList} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

已选中 {checkedList.length} 条

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} checkedState={[checkedList, setCheckedList]} />
); }; const ExecutionTargetItem = ({ info, executionData, setIsShowErrMsg }) => { const [data, setData] = executionData; return (
{info.instance_name} { let result = data.filter((i) => !i.id == info.id); if (result.length == 0) { setIsShowErrMsg(true); } setData(() => { return result; }); }} />
); }; export default ToolExecution; ================================================ FILE: omp_web/src/pages/ToolExecution/index.module.less ================================================ .header { height: 54px; display: flex; align-items: center; border-bottom: 1px solid #d6d6d6; padding-top: 5px; padding-left: 20px; font-size: 17px; color: #222222; justify-content: space-between; padding-right: 35px; } ================================================ FILE: omp_web/src/pages/ToolExecutionResults/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpMessageModal, OmpSelect, OmpDatePicker, OmpDrawer, } from "@/components"; import { Button, Collapse, Tooltip, Table, Spin } from "antd"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import styles from "./index.module.less"; import { CaretRightOutlined, QuestionCircleOutlined, DownloadOutlined, } from "@ant-design/icons"; import { useEffect, useRef, useState } from "react"; import { useHistory, useLocation } from "react-router-dom"; import { handleResponse, _idxInit, refreshTime, downloadFile, } from "@/utils/utils"; import moment from "moment"; const { Panel } = Collapse; const statusTextMap = [ "等待执行", "执行中", "执行成功", "执行失败", "任务超时", ]; const statusColorMap = ["#ffbf00", "#ffbf00", "#76ca68", "#f04134", "#f04134"]; const ToolExecutionResults = () => { const history = useHistory(); const locationArr = useLocation().pathname.split("/"); const [loading, setLoading] = useState(false); const [info, setInfo] = useState({}); // 当前选中的ip const [currentIp, setCurrentIp] = useState(null); // 当前选中的状态 const [currentStatus, setCurrentStatus] = useState(null); const timer = useRef(null); const queryData = (init) => { init && setLoading(true); fetchGet( `${apiRequest.utilitie.queryResult}${ locationArr[locationArr.length - 1] }/` ) .then((res) => { handleResponse(res, (res) => { setInfo(res.data); if (res.data.tool_detail) { currentIp == null && setCurrentIp(res.data.tool_detail[0].ip); currentStatus == null && setCurrentStatus(res.data.tool_detail[0].status); } // 等待执行 和 执行中 要继续请求 if (res.data.status == 0 || res.data.status == 1) { timer.current = setTimeout(() => { queryData(); }, 5000); } }); }) .catch((e) => console.log(e)) .finally(() => { init && setLoading(false); }); }; // 执行结果当前选中的项 const currentItem = info.tool_detail?.filter((i) => i.ip == currentIp)[0]; // 确定执行结果的tab状态 const tabRenderStatus = (status) => { return info.tool_detail?.filter((i) => i.status == status).length; }; useEffect(() => { queryData("init"); return ()=>{ timer.current && clearTimeout(timer.current) } }, []); return (
{info.task_name || "-"}{" "} {statusTextMap[info.status]}
history?.goBack()}> 返回
{}} style={{ marginTop: 0, border: "none", backgroundColor: "#fff" }} expandIcon={({ isActive }) => ( )} >
操作用户{" "}
{info.operator || "-"}
执行对象{" "}
{info.tool?.target_name || "-"}
目标数量{" "}
{info.count || "-"}
执行用户
{" "} {info.run_user || "-"}
超时时间 (s)
{info.time_out || "-"}
开始时间
{info.start_time ? moment(info.start_time).format("YYYY-MM-DD HH:mm:ss") : "-"}
结束时间
{" "} {info.end_time ? moment(info.end_time).format("YYYY-MM-DD HH:mm:ss") : "-"}
总耗时
{info.duration || "-"}
text || "-", }, { title: "参数值", key: "value", dataIndex: "value", width: 120, align: "center", // render: (text) => (text ? argType[text] : "-"), }, ]} pagination={false} dataSource={info.tool_args} />
{ if (tabRenderStatus(0) !== 0) { setCurrentStatus(0); setCurrentIp( info.tool_detail?.filter((i) => i.status == 0)[0].ip ); } }} > 待执行({tabRenderStatus(0)})
{ if (tabRenderStatus(1) !== 0) { setCurrentStatus(1); setCurrentIp( info.tool_detail?.filter((i) => i.status == 1)[0].ip ); } }} > 执行中({tabRenderStatus(1)})
{ if (tabRenderStatus(2) !== 0) { setCurrentStatus(2); setCurrentIp( info.tool_detail?.filter((i) => i.status == 2)[0].ip ); } }} > 执行成功({tabRenderStatus(2)})
{ if (tabRenderStatus(3) !== 0) { setCurrentStatus(3); setCurrentIp( info.tool_detail?.filter((i) => i.status == 3)[0].ip ); } }} > 执行失败({tabRenderStatus(3)})
{ if (tabRenderStatus(4) !== 0) { setCurrentStatus(4); setCurrentIp( info.tool_detail?.filter((i) => i.status == 4)[0].ip ); } }} > 任务超时({tabRenderStatus(4)})
{info.tool_detail ?.filter((i) => i.status == currentStatus) .map((item) => { return (
{ setCurrentIp(item.ip); }} style={{ cursor: "pointer", padding: "10px 0px", backgroundColor: currentIp == item.ip ? "#2f7bed" : "#fff", color: currentIp == item.ip ? "#fff" : "#37474d", textAlign: "center", }} > {item.ip}
); })}
{currentItem && currentItem.url && ( )}
{currentItem?.log}
); }; export default ToolExecutionResults; ================================================ FILE: omp_web/src/pages/ToolExecutionResults/index.module.less ================================================ .resultTitle { display: flex; justify-content: space-between; height: 62px; // background-color: red; line-height: 62px; padding-left: 0px; font-size: 18px; color: rgb(34, 34, 34); .resultTitleStatus { font-size: 14px; height: 62px; display: block; padding-top: 1px; padding-left: 20px; } } .panelItem { background: #f6f6f6; border-radius: 4px; border: 0; overflow: hidden; border-bottom: 0 !important; } .panelItem > div:nth-child(1) { padding: 8px 15px; } .panelItem > div:nth-child(2) > div { background-color: #fff !important; padding: 0; } .baseTable { margin-top: 10px; border: 1px solid #d6d6d6; .baseTableFirstRow { display: flex; width: 100%; height: 42px; .baseTableItem { flex: 1; display: flex; align-items: center; border-right: 1px solid #d6d6d6; .baseTableItemLabel { flex: 2; border-right: 1px solid #d6d6d6; height: 100%; line-height: 42px; padding-left: 15px; background-color: #f6f6f6; font-weight: 500; } .baseTableItemContent { flex: 3; padding-left: 15px; } } } .baseTableSecondRow { display: flex; width: 100%; height: 42px; .baseTableItem { flex: 1; display: flex; align-items: center; border-top: 1px solid #d6d6d6; border-right: 1px solid #d6d6d6; .baseTableItemLabel { flex: 2; border-right: 1px solid #d6d6d6; height: 100%; line-height: 42px; padding-left: 15px; background-color: #f6f6f6; font-weight: 500; } .baseTableItemContent { flex: 3; padding-left: 15px; } } } } ================================================ FILE: omp_web/src/pages/ToolManagement/config/card.js ================================================ import styles from "./index.module.less"; import { useState } from "react"; import initLogo from "../initLogo/tools.svg"; const kindMap = ["管理工具", "检查工具", "安全工具", "其他工具"]; const Card = ({ idx, history, info, tabKey }) => { const [isHover, setIsHover] = useState(false); // let href = window.location.href.split("#")[0]; // console.log(href) return (
{ setIsHover(true); }} onMouseLeave={() => { setIsHover(false); }} onClick={() => { history?.push({ pathname: `/utilitie/tool-management/tool-management-detail/${info.id}`, }); }} >
{!info.logo ? (
{/* {info.name && info.name[0].toLocaleUpperCase()} */}
) : (
{/* {info.name && info.name[0].toLocaleUpperCase()} */}
)}
{info.name}
{}} >
[ {kindMap[info.kind]} ] 使用次数:{info.used_number}

{/* */} {info.description} {/* */}

); }; const InitLogo = ({ name }) => { return (
{name && name[0].toLocaleUpperCase()}
); }; export default Card; ================================================ FILE: omp_web/src/pages/ToolManagement/config/index.module.less ================================================ .cardContainer:hover { // top: -4px !important; box-shadow: 0px 2px 5px rgba(0, 0, 0, 0.3); color: black !important; } .cardContainer { border-radius: 4px; cursor: pointer; .cardContent { height: 100%; // display: flex; padding-top: 10px; .text { font-size: 12px; display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical; // height: r(210); // font-size: r(42); // white-space: nowrap; text-overflow: ellipsis; overflow: hidden; width: 100%; margin: 0; color: #4f4f4f; padding-left: 10px; } } .cardBtn { display: flex; //justify-content: space-around; border-top: solid 1px #e7e7e7; align-items: center; height: 24%; // /color: #818181; font-size: 13px; & > div { width: 50%; text-align: center; } // & > div:hover { // color: rgb(46, 124, 238); // } } } .detailContainer { background-color: #fff; padding-top: 10px; padding-left: 3px; padding-right: 3px; .detailHeader { overflow: hidden; background-color: #f5f6f5; height: 40px; line-height: 40px; padding-left: 20px; display: flex; justify-content: space-between; } .detailTitle { height: 120px; display: flex; padding-left: 20px; padding-top: 0px; padding-right: 200px; // background-color: aliceblue; align-items: center; .detailTitleDescribe { height: 80px; padding-left: 60px; //word-wrap: break-word; width: calc(100% - 120px); color: #4f4f4f; position: relative; .detailTitleDescribeText { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; //height: r(210); //font-size: r(42); // white-space: nowrap; text-overflow: ellipsis; overflow: hidden; // /width: 100%; } } } .detailContent { color: #4f4f4f; padding-left: 30px; .detailContentItem { display: flex; padding-top: 15px; .detailContentItemLabel { width: 120px; } } } .detailDependence { padding-left: 30px; margin-top: 40px; font-size: 16px; //background-color: red; .detailDependenceTable { //width: 100%; margin-right: 20px; border: 1px solid #d6d6d6; margin-top: 20px; } } } .backIcon:hover { color: rgb(46, 124, 238); } ================================================ FILE: omp_web/src/pages/ToolManagement/detail/Readme.js ================================================ import * as markdown from "markdown-it"; import * as colors from "markdown-it-colors"; import hljs from "highlight.js"; import "highlight.js/styles/monokai-sublime.css"; console.log(hljs); const Readme = ({ text = "" }) => { var md = markdown({ gfm: true, pedantic: false, sanitize: false, tables: true, breaks: false, smartLists: true, smartypants: false, highlight: function (code) { return hljs.highlightAuto(code).value; }, }); console.log(md); console.log(markdown()); return (
); }; export default Readme; ================================================ FILE: omp_web/src/pages/ToolManagement/detail/index.js ================================================ import { OmpContentWrapper } from "@/components"; import { Button, Table, Tooltip, Spin } from "antd"; import { useState, useEffect } from "react"; import { handleResponse, _idxInit, downloadFile } from "@/utils/utils"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { QuestionCircleOutlined, FileProtectOutlined } from "@ant-design/icons"; import { useHistory, useLocation } from "react-router-dom"; import styles from "./index.module.less"; import Readme from "./Readme.js"; import initLogo from "../initLogo/tools.svg"; const kindMap = ["管理工具", "检查工具", "安全工具", "其他工具"]; const argType = { select: "单选", file: "文件", input: "单行文本", }; const Details = () => { const history = useHistory(); const [loading, setLoading] = useState(false); // console.log([...location].pop()) // let href = window.location.href.split("#")[0]; const locationArr = useLocation().pathname.split("/"); const [info, setInfo] = useState({}); const queryInfo = () => { setLoading(true); fetchGet( `${apiRequest.utilitie.queryList}${locationArr[locationArr.length - 1]}/` ) .then((res) => { handleResponse(res, (res) => { setInfo(res.data); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { queryInfo(); }, []); return (
{!info.logo ? (
{/* {info.name && info.name[0].toLocaleUpperCase()} */}
) : (
{/* {info.name && info.name[0].toLocaleUpperCase()} */}
)}
{info.name}
{info.description}
工具类别
{kindMap[info.kind]}
{/*
执行位置
目标主机
*/}
执行对象
{info.target_name == "host" ? "主机" : info.target_name}
执行参数
text || "-", }, { title: "类型", key: "type", dataIndex: "type", align: "center", render: (text) => (text ? argType[text] : "-"), }, { title: "默认值", key: "default", dataIndex: "default", align: "center", render: (text) => text || "-", }, { title: "必填字段", key: "required", dataIndex: "required", align: "center", render: (text) => `${text}`, }, ]} pagination={false} dataSource={info.script_args} /> {info && info.templates && info.templates.length > 0 && (
下载示例文件
{ return ( { downloadFile(`/${text}`); }} > 下载 ); }, }, ]} pagination={false} dataSource={info.templates} /> )}
README.md
); }; export default Details; ================================================ FILE: omp_web/src/pages/ToolManagement/detail/index.module.less ================================================ .header { display: flex; height: 115px; .icon { width: 100px; height: 100px; background-color: #f5f5f5; border-radius: 5px; padding: 10px; display: flex; align-items: center; justify-content: center; } .headerContent { flex: 1; height: 100px; padding-left: 15px; .headerContentTitle { font-size: 17px; padding-bottom: 10px; color: rgb(34, 34, 34); } .headerContentDescribe { padding-bottom: 10px; // padding-top: 8px; } // .headerContentBtn { // } } .headerBtn { width: 80px; } } .detailInfo { background-color: #f5f5f5; margin-top: 10px; border-radius: 5px; border: 1px solid #e9e9e9; padding: 10px; .detailItem { display: flex; padding-bottom: 25px; .detailItemLabel { width: 180px; color: rgb(34, 34, 34); } } } .detailContent { padding-top: 20px; .detailContentTitle { color: rgb(34, 34, 34); } .tableContainer { border: 1px solid #d6d6d6; margin-top: 20px; } } .readme { border: 1px solid #d6d6d6; margin-top: 20px; .readmeTitle { background-color: #f6f6f6; height: 45px; line-height: 45px; border-bottom: 1px solid #d6d6d6; padding-left: 20px; } .readmeContent { padding: 20px; padding-top: 20px; // background-color: rgba(210,210,210,.2); .bread-div { padding: 0.5rem; border-bottom: 1px solid #eee; background-color: #e1f0ff; } .detailed-title { font-size: 1.8rem; text-align: center; padding: 1rem; } .center { text-align: center; } .detailed-content { padding: 1.3rem; font-size: 1rem; } pre { display: block; background-color: #f3f3f3; padding: 0.5rem !important; overflow-y: auto; font-weight: 300; font-family: Menlo, monospace; border-radius: 0.3rem; } pre { background-color: #283646 !important; } pre > code { border: 0px !important; background-color: #283646 !important; color: #fff; } code { display: inline-block; background-color: #f3f3f3; border: 1px solid #fdb9cc; border-radius: 3px; font-size: 12px; padding-left: 5px; padding-right: 5px; color: #4f4f4f; margin: 0px 3px; } .title-anchor { color: #888 !important; padding: 4px !important; margin: 0rem !important; height: auto !important; line-height: 1.2rem !important; font-size: 0.7rem !important; border-bottom: 1px dashed #eee; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .active { color: rgb(30, 144, 255) !important; } .nav-title { text-align: center; color: #888; border-bottom: 1px solid rgb(30, 144, 255); } .article-menu { font-size: 12px; } iframe { height: 34rem; } .detailed-content img { width: 100%; border: 1px solid #f3f3f3; } .title-level3 { display: none !important; } .ant-anchor-link-title { font-size: 12px !important; } .ant-anchor-wrapper { padding: 5px !important; } code, pre { border-radius: 3px; background-color: #f7f7f7; color: inherit; } code { font-family: Consolas, Monaco, Andale Mono, monospace; margin: 0 2px; } pre { line-height: 1.7em; overflow: auto; padding: 6px 10px; // border-left: 5px solid #6ce26c; } pre > code { border: 0; display: inline; max-width: initial; padding: 0; margin: 0; overflow: initial; line-height: inherit; font-size: 0.85em; white-space: pre; background: 0 0; } code { color: #666555; } table { *border-collapse: collapse; /* IE7 and lower */ border-spacing: 0; width: 100%; } table { border:solid #d6d6d6 1px; -moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; /*-webkit-box-shadow: 0 1px 1px #ccc; -moz-box-shadow: 0 1px 1px #ccc; box-shadow: 0 1px 1px #ccc; */ } table tr:hover { background: #fbf8e9; -o-transition: all 0.1s ease-in-out; -webkit-transition: all 0.1s ease-in-out; -moz-transition: all 0.1s ease-in-out; -ms-transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out; } table td, .table th { border-left: none; border-top: 1px solid #f0f0f0; padding: 10px; text-align: center; border-bottom: 1px solid #f0f0f0; font-size: 12px; } table th { background-color: #f9fafd; // background-image: -webkit-gradient( // linear, // left top, // left bottom, // from(#ebf3fc), // to(#dce9f9) // ); // background-image: -webkit-linear-gradient(top, #ebf3fc, #dce9f9); // background-image: -moz-linear-gradient(top, #ebf3fc, #dce9f9); // background-image: -ms-linear-gradient(top, #ebf3fc, #dce9f9); // background-image: -o-linear-gradient(top, #ebf3fc, #dce9f9); // background-image: linear-gradient(top, #ebf3fc, #dce9f9); /*-webkit-box-shadow: 0 1px 0 rgba(255,255,255,.8) inset; -moz-box-shadow:0 1px 0 rgba(255,255,255,.8) inset; box-shadow: 0 1px 0 rgba(255,255,255,.8) inset;*/ border-top: none; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); padding: 5px; padding-top: 12px; padding-bottom: 12px; font-size: 12px; } table td:first-child, table th:first-child { border-left: none; } table th:first-child { -moz-border-radius: 6px 0 0 0; -webkit-border-radius: 6px 0 0 0; border-radius: 6px 0 0 0; } table th:last-child { -moz-border-radius: 0 6px 0 0; -webkit-border-radius: 0 6px 0 0; border-radius: 0 6px 0 0; } table th:only-child { -moz-border-radius: 6px 6px 0 0; -webkit-border-radius: 6px 6px 0 0; border-radius: 6px 6px 0 0; } table tr:last-child td { -moz-border-radius: 0 0 6px 0; -webkit-border-radius: 0 0 6px 0; border-radius: 0 0 6px 0; border-bottom: solid #d6d6d6 1px; } } } ================================================ FILE: omp_web/src/pages/ToolManagement/index.js ================================================ import { Input, Pagination, Empty, Spin } from "antd"; import { useEffect, useState } from "react"; import styles from "./index.module.less"; import { SearchOutlined } from "@ant-design/icons"; import Card from "./config/card.js"; import { useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; import { fetchGet } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; import { handleResponse } from "@/utils/utils"; const ToolManagement = () => { // 视口高度 const viewHeight = useSelector((state) => state.layouts.viewSize.height); const history = useHistory(); const [tabKey, setTabKey] = useState(); const [searchName, setSearchName] = useState(""); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [pagination, setPagination] = useState({ current: 1, pageSize: viewHeight > 955 ? 16 : 12, total: 0, searchParams: {}, }); function fetchData(pageParams = { current: 1, pageSize: 8 }, searchParams) { setLoading(true); fetchGet(apiRequest.utilitie.queryList, { params: { page: pageParams.current, size: pageParams.pageSize, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { // 获得真正的总数,要查询条件都为空时 let obj = { ...searchParams }; delete obj.tabKey; let arr = Object.values(obj).filter((i) => i); if (arr.length == 0) { setTotal(res.data.count); } setDataSource(res.data.results); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { location.state = {}; setLoading(false); // fetchSearchlist(); //fetchIPlist(); }); } useEffect(() => { fetchData( { current: 1, pageSize: pagination.pageSize }, { ...pagination.searchParams, kind: tabKey, } ); }, [tabKey]); return (
{ setPagination({ current: 1, pageSize: viewHeight > 955 ? 16 : 12, total: 0, searchParams: {}, }); if (e.target.innerHTML == "全部") { setTabKey(); } else if (e.target.innerHTML == "管理工具") { setTabKey(0); } else if (e.target.innerHTML == "安全工具") { setTabKey(2); } }} >
全部
|
管理工具
|
安全工具
} style={{ marginRight: 10, width: 280 }} value={searchName} allowClear onChange={(e) => { setSearchName(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, kind: tabKey, name: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, name: searchName, kind: tabKey, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, name: searchName, kind: tabKey, } ); }} />

{/*

全部

*/}
{/* */} 共收录 {total} 个实用工具
{dataSource.length == 0 ? ( 955 ? 500 : 300, flexDirection: "column", }} description={"暂无使用工具"} /> ) : ( <> {dataSource.map((item, idx) => { return ( ); })} )}
{dataSource.length !== 0 && (
{ fetchData( { ...pagination, current: e }, { ...pagination.searchParams, kind: tabKey, } ); }} current={pagination.current} pageSize={pagination.pageSize} total={pagination.total} />
)}
); }; export default ToolManagement; ================================================ FILE: omp_web/src/pages/ToolManagement/index.module.less ================================================ .header { background-color: #fff; padding-bottom: 14px; //display: flex; //padding:20px; .headerTabRow { display: flex; //align-items: center; justify-content: space-between; .headerTab { display: flex; color: #4f4f4f; font-size: 14px; margin-top: 20px; & > div:nth-child(1) { padding-left: 20px; //margin-top: 20px; cursor: pointer; } & > div:nth-child(2) { padding-left: 10px; padding-right: 10px; //margin-top: 20px; color: #b9b9b9; } & > div:nth-child(3) { //margin-top: 20px; cursor: pointer; } & > div:nth-child(4) { padding-left: 10px; padding-right: 10px; //margin-top: 20px; color: #b9b9b9; } & > div:nth-child(3) { // margin-top: 20px; cursor: pointer; } & > div:nth-child(5) { // margin-top: 20px; cursor: pointer; } } .headerBtn { display: flex; padding-top: 10px; padding-bottom: 5px; position: relative; top: 3px; padding-right:20px } } .headerHr { border-top: solid 1px #e7e7e7; border-left: solid 1px #e7e7e7; } .headerSearch { padding-left: 20px; display: flex; justify-content: space-between; font-size: 12px; .headerSearchCondition { position: relative; top:5px; display: flex; & > p { padding-right: 20px; cursor: pointer; } } .headerSearchInfo { color: #818181; padding-right: 10px; } } } ================================================ FILE: omp_web/src/pages/UserManagement/index.js ================================================ import { OmpContentWrapper, OmpTable, OmpModal } from "@/components"; import { Button, Input, Form, message } from "antd"; import { useState, useEffect, useRef } from "react"; import { handleResponse, _idxInit, refreshTime, nonEmptyProcessing, logout, isPassword, encrypt, } from "@/utils/utils"; import { fetchGet, fetchPost } from "@/utils/request"; import { apiRequest } from "@/config/requestApi"; //import updata from "@/store_global/globalStore"; import { useDispatch } from "react-redux"; import moment from "moment"; import { SearchOutlined } from "@ant-design/icons"; const UserManagement = () => { const dispatch = useDispatch(); const [loading, setLoading] = useState(false); const [searchLoading, setSearchLoading] = useState(false); //table表格数据 const [dataSource, setDataSource] = useState([]); const [userListSource, setUserListSource] = useState([]); const [searchValue, setSearchValue] = useState(""); const [selectValue, setSelectValue] = useState(); const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0, ordering: "", searchParams: {}, }); //修改密码弹框 const [showModal, setShowModal] = useState(false); const columns = [ { title: "序列", width: 40, key: "_idx", dataIndex: "_idx", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, fixed: "left", }, { title: "用户名", key: "username", width: 100, dataIndex: "username", //sorter: (a, b) => a.username - b.username, // sortDirections: ["descend", "ascend"], align: "center", render: nonEmptyProcessing, }, { title: "角色", key: "is_superuser", dataIndex: "is_superuser", width: 100, //sorter: (a, b) => a.is_superuser - b.is_superuser, //sortDirections: ["descend", "ascend"], align: "center", render: (text, record) => { if (text) { return "普通管理员"; } else { if (record.username == "omp") { return "只读用户"; } return "普通用户"; } }, }, { title: "用户状态", key: "is_active", dataIndex: "is_active", align: "center", width: 100, render: (text) => { if (text) { return "正常"; } else { return "停用"; } }, }, { title: "创建时间", key: "date_joined", dataIndex: "date_joined", align: "center", width: 100, render: (text) => { if (text) { return moment(text).format("YYYY-MM-DD HH:mm:ss"); } else { return "-"; } }, }, // { // title: "描述", // key: "describe", // dataIndex: "describe", // align: "center", // render: nonEmptyProcessing, // }, { title: "用户操作", key: "1", width: 50, dataIndex: "1", align: "center", fixed: "right", render: function renderFunc(text, record, index) { return (
{ setRow(record); setShowModal(true); }} style={{ display: "flex", justifyContent: "space-around" }} > 修改密码
); }, }, ]; const msgRef = useRef(null); //select 的onblur函数拿不到最新的search value,使用useref存(是最新的,但是因为失去焦点时会自动触发清空search,还是得使用ref存) const searchValueRef = useRef(null); // 定义row存数据 const [row, setRow] = useState({}); //auth/users function fetchData( pageParams = { current: 1, pageSize: 10 }, searchParams, ordering ) { setLoading(true); fetchGet(apiRequest.auth.users, { params: { page: pageParams.current, size: pageParams.pageSize, ordering: ordering ? ordering : null, ...searchParams, }, }) .then((res) => { handleResponse(res, (res) => { if (!searchParams) { setUserListSource(res.data.results.map((item) => item.username)); } setDataSource( res.data.results.map((item, idx) => { return { ...item, _idx: idx + 1 + (pageParams.current - 1) * pageParams.pageSize, }; }) ); setPagination({ ...pagination, total: res.data.count, pageSize: pageParams.pageSize, current: pageParams.current, ordering: ordering, searchParams: searchParams, }); }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); } const onPassWordChange = (data) => { setLoading(true); fetchPost(apiRequest.auth.changePassword, { body: { username: encrypt(row.username), old_password: encrypt(data.old_password), new_password: encrypt(data.new_password2), }, }) .then((res) => { handleResponse(res, (res) => { if (res.code == 0) { if (localStorage.getItem("username") == row.username) { message.success("修改密码成功, 请重新登录"); setTimeout(() => { logout(); }, 1000); } else { message.success("修改密码成功"); } setShowModal(false); } }); }) .catch((e) => console.log(e)) .finally(() => { setLoading(false); }); }; useEffect(() => { fetchData(pagination); }, []); //console.log(checkedList) // 防止在校验进入死循环 const flag = useRef(null); return (
用户名: {/* { setSelectValue(e) console.log(e) fetchData( { current: pagination.current, pageSize: pagination.pageSize }, {username:e}, pagination.ordering ); }} style={{ width: 200 }} /> */} { setSelectValue(e.target.value); if (!e.target.value) { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: null, } ); } }} onBlur={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, } ); }} onPressEnter={() => { fetchData( { current: 1, pageSize: pagination.pageSize, }, { ...pagination.searchParams, username: selectValue, }, pagination.ordering ); }} suffix={ !selectValue && ( ) } />
{ let ordering = sorter.order ? `${sorter.order == "descend" ? "" : "-"}${sorter.columnKey}` : null; setTimeout(() => { fetchData(e, pagination.searchParams, ordering); }, 200); }} columns={columns} dataSource={dataSource} pagination={{ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => (

共计{" "} {pagination.total} {" "} 条

), ...pagination, }} rowKey={(record) => record.id} />
{ flag.current = true; }} afterClose={() => { flag.current = null; }} > { if (value) { if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } return Promise.resolve("success"); } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} > { if (value) { if (!flag.current) { passwordModalForm.validateFields(["new_password2"]); } if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } return Promise.resolve("success"); } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} > { if (value) { if (!isPassword(value)) { if (value.length < 8) { return Promise.reject("密码长度需大于8位"); } if ( passwordModalForm.getFieldValue().new_password1 === value || !value ) { return Promise.resolve("success"); } else { return Promise.reject("两次密码输入不一致"); } } else { return Promise.reject( `密码只支持数字、字母以及常用英文符号` ); } } else { return Promise.resolve("success"); } }, }, ]} >
); }; export default UserManagement; ================================================ FILE: omp_web/src/react-app-env.d.ts ================================================ /// ================================================ FILE: omp_web/src/router.js ================================================ import { HashRouter as Router, Route, Switch, Redirect } from "react-router-dom"; import OmpLayout from "@/layouts"; import Login from "@/pages/Login"; import routerConfig from "@/config/router.config"; import HomePage from "@/pages/HomePage"; const OmpRouter = () => { let routerChildArr = routerConfig.map(item=>item.children).flat() return ( } /> ( } /> {routerChildArr.map((item) => { return ( } /> ); })} )} /> ] ); }; export default OmpRouter; ================================================ FILE: omp_web/src/store_redux/reducer.js ================================================ import { combineReducers } from "redux"; import { reducer as customBreadcrumbReducer } from "@/components/CustomBreadcrumb/store"; import { reducer as layoutsReducer } from "@/layouts/store"; //import { reducer as warningRecordReducer } from "@/pages/OperationManagement/WarningRecord/store"; import { reducer as systemManagementReducer } from "@/pages/SystemManagement/store"; import { reducer as appStoreReducer } from "@/pages/AppStore/store"; import { reducer as installReducer } from "@/pages/AppStore/config/Installation/store"; const cReducer = combineReducers({ customBreadcrumb: customBreadcrumbReducer, layouts: layoutsReducer, // warningRecord:warningRecordReducer, systemManagement: systemManagementReducer, appStore: appStoreReducer, installation: installReducer, }); export default cReducer; ================================================ FILE: omp_web/src/store_redux/reduxStore.js ================================================ import { createStore } from "redux"; import reducer from "./reducer"; const store = createStore(reducer); export default store; ================================================ FILE: omp_web/src/utils/index.module.less ================================================ ._bigfontSize{ font-size: 14px; } .listButton { display: flex; justify-content: center; color: #1890ff; cursor: pointer; & > div:not(:last-child) { margin-right: 10px; } } .loginMessageShow { opacity: 1; transition: all .2s ease-in; } .loginMessageHide{ opacity: 0; transition: all .2s ease-in; //animation: hide-item 2s ease-in forwards; } ================================================ FILE: omp_web/src/utils/request.js ================================================ import axios from "axios"; import { logout } from "./utils" const getBaseUrl = (env) => { let base; if (!base) { base = "/"; } return base; }; class NewAxios { constructor() { this.baseURL = getBaseUrl(process.env.NODE_ENV); this.timeout = 150000; this.withCredentials = true; } setInterceptors = (instance, url) => { instance.interceptors.request.use( (config) => { // 在这里添加loading // 配置token return config; }, (err) => Promise.reject(err) ); instance.interceptors.response.use( (response) => { // 在这里移除loading // todo: 想根据业务需要,对响应结果预先处理的,都放在这里 return response; }, (err) => { if (err.response) { // 响应错误码处理 console.log(err.response) switch (err.response.status) { case 403: // todo: handler server forbidden error break; case 401: logout() // todo: handler server forbidden error break; // todo: handler other status code default: break; } return Promise.reject(err.response); } if (!window.navigator.onLine) { // 断网处理 // todo: jump to offline page return -1; } return Promise.reject(err); } ); }; request(options) { // 每次请求都会创建新的axios实例。 const instance = axios.create(); const config = { // 将用户传过来的参数与公共配置合并。 ...options, baseURL: this.baseURL, timeout: this.timeout, withCredentials: this.withCredentials, }; // 配置拦截器,支持根据不同url配置不同的拦截器。 this.setInterceptors(instance, options.url); return instance(config); // 返回axios实例的执行结果 } } //为了保持和之前项目请求方式一样 //export const fetchPost = new NewAxios() export const fetchPost = (url, params) => new NewAxios().request({ url: url, method: "POST", data: { ...params?.body, }, }); export const fetchGet = (url, params) => new NewAxios().request({ url: url, method: "GET", params: { ...params?.params, }, }); export const fetchPut = (url, params) => new NewAxios().request({ url: url, method: "PUT", data: { ...params?.body, }, }); export const fetchDelete = (url, params) => new NewAxios().request({ url: url, method: "Delete", params: { ...params?.params, }, }); export const fetchPatch = (url, params) => new NewAxios().request({ url: url, method: "Patch", data: { ...params?.body, }, }); ================================================ FILE: omp_web/src/utils/utils.js ================================================ //import { ColorfulNotice } from "@/components"; import { Badge, message, Tooltip } from "antd"; import moment from "moment"; import * as R from "ramda"; import styles from "./index.module.less"; import { getRefreshTimeChangeAction } from "@/components/CustomBreadcrumb/store/actionsCreators"; import { CloseCircleFilled, CheckCircleFilled } from "@ant-design/icons"; import JSEncrypt from "jsencrypt"; /** * 正常/绿色 bg"rgb(238, 250, 244)" bo:"rgb(84, 187, 166)" * 异常/红色 "#da4e48", "#fbe7e6" * 警告/黄色 rgba(247, 231, 24,.2)" borderColor="#f5c773" */ /** * 统一的分页配置项 * @param data * @returns {{total, pageSizeOptions: [string, string, string, string], showTotal: (function(): string), showSizeChanger: boolean}} */ /*eslint-disable*/ export const paginationConfig = (data) => ({ showSizeChanger: true, pageSizeOptions: ["10", "20", "50", "100"], showTotal: () => ( 共计 {data.length} ), total: data.length, // onShowSizeChange: (current, pageSize) => this.changePageSize(pageSize, current), // onChange: (current) => this.changePage(current), }); /*eslint-disable*/ export const isTableTextInvalid = (text) => String(text) === "null" || text === "" || text === undefined; /** * 格式化 table 渲染项 * @param text * @param record * @param index * @returns {JSX.Element|string|*} */ export function formatTableRenderData(text, record, index) { if (isTableTextInvalid(text)) { return "-"; } else if (text === 0 || text === "active" || text === true) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}正常
); } else if (text === 1 || text === "unactive" || text === false) { return
{renderCircular("#da4e48", "#fbe7e6")}异常
; } else if (text === "CREATE") { return "增加"; } else if (text === "UPDATE") { return "更新"; } else if (text === "DELETE") { return "删除"; } else { // /console.log("text",text); return text; } } function report_service_RenderData(text, record, index) { if (text) { return text; } else { return "-"; } } export function renderFormattedTime(text, record, index) { { if (isTableTextInvalid(text)) { return "-"; } else { let duration = ""; const second = Math.round(Number(text)), days = Math.floor(second / 86400), hours = Math.floor((second % 86400) / 3600), minutes = Math.floor(((second % 86400) % 3600) / 60), seconds = Math.floor(((second % 86400) % 3600) % 60); if (days > 0) { duration = days + "天" + hours + "小时"; } else if (hours > 0) { duration = hours + "小时" + minutes + "分"; } else if (minutes > 0) { duration = minutes + "分"; } else if (seconds > 0) { duration = seconds + "秒"; } return duration; } } } export function renderInformation(text, record, index) { const { cpu, memory, disk } = record; const unit = 1024 * 1024 * 1024; const cpuText = isTableTextInvalid(cpu) ? "-" : `${cpu}C`; const memoryText = isTableTextInvalid(memory) ? "-" : `${(memory / unit).toFixed(1)}G`; const diskText = isTableTextInvalid(disk) ? "-" : `${(disk / unit).toFixed(1)}G`; if (cpuText === "-" && memoryText === "-" && diskText === "-") { return "-"; } return `${cpuText}|${memoryText}|${diskText}`; } //小圆点 export const renderCircular = (borderColor, backgroundColor) => { return ( ); }; export function ColorfulNotice({ backgroundColor, borderColor, text, top, width = 55, }) { //if(top)console.log("top",top); return (
{text}
); } /** * table组件中ip排序 * @param a * @param b * @returns {number} */ export const tableSorter = { sortIP: (a, b) => { if (!a.ip || !b.ip) return 0; const ip1 = a.ip .split(".") .map((el) => el.padStart(3, "0")) .join(""); const ip2 = b.ip .split(".") .map((el) => el.padStart(3, "0")) .join(""); return ip1 - ip2; }, sortAlertIP: (a, b) => { if (!a.alert_host_ip || !b.alert_host_ip) return 0; const ip1 = a.alert_host_ip .split(".") .map((el) => el.padStart(3, "0")) .join(""); const ip2 = b.alert_host_ip .split(".") .map((el) => el.padStart(3, "0")) .join(""); return ip1 - ip2; }, sortUsageRate: (a, b) => { return ( Number(isTableTextInvalid(a) ? 0 : a) - Number(isTableTextInvalid(b) ? 0 : b) ); }, }; export function renderToolTip(text) { return (
{text}
); } // 汇总了所有无需额外逻辑的 table配置项 export const columnsConfig = { idx: { title: "序列", key: "index", render: (text, record, index) => `${index + 1}`, align: "center", width: 60, //ixed: "left", }, product_name: { title: "服务类型", //width: 150, key: "product_name", dataIndex: "product_name", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.product_name); const str2 = R.defaultTo(" ", b.product_name); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, functional_module: { title: "功能模块", width: 120, key: "product_cn_name", dataIndex: "product_cn_name", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.product_cn_name); const str2 = R.defaultTo(" ", b.product_cn_name); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, alert_service_type: { title: "功能模块", width: 120, key: "alert_service_type", dataIndex: "alert_service_type", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.alert_service_type); const str2 = R.defaultTo(" ", b.alert_service_type); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, service_name: { title: "服务名称", width: 180, key: "service_name", dataIndex: "service_name", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.service_name); const str2 = R.defaultTo(" ", b.service_name); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, alert_service_name: { title: "服务名称", width: 160, key: "alert_service_name", dataIndex: "alert_service_name", ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.alert_service_name); const str2 = R.defaultTo(" ", b.alert_service_name); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, ip: { title: "IP地址", width: 160, key: "ip", dataIndex: "ip", //ellipsis: true, sorter: tableSorter.sortIP, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, thirdParty_ip: { title: "连接地址", //width: 120, key: "ip", dataIndex: "ip", //ellipsis: true, sorter: tableSorter.sortIP, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, alert_host_ip: { title: "IP地址", width: 80, key: "alert_host_ip", dataIndex: "alert_host_ip", //ellipsis: true, sorter: tableSorter.sortAlertIP, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, alert_level: { title: "告警级别", width: 100, key: "alert_level", dataIndex: "alert_level", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.alert_level); const str2 = R.defaultTo(" ", b.alert_level); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, render: function renderFunc(text, record, index) { switch (record.alert_level) { case "critical": return ( ); case "warning": return ( ); default: return "-"; } }, sortDirections: ["descend", "ascend"], align: "center", }, alert_describe: { title: "告警描述", key: "alert_describe", dataIndex: "alert_describe", width: 280, align: "center", ellipsis: true, render: renderToolTip, }, alert_time: { title: "告警时间", width: 140, key: "alert_time", dataIndex: "alert_time", //ellipsis: true, sorter: (a, b) => moment(a.alert_time).valueOf() - moment(b.alert_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, // 告警记录中的告警时间,使用创建时间字段 warning_record_alert_time: { title: "告警时间", width: 180, key: "alert_time", dataIndex: "create_time", //ellipsis: true, sorter: (a, b) => moment(a.create_time).valueOf() - moment(b.create_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, alert_receiver: { title: "告警推送", //width: 150, key: "alert_receiver", dataIndex: "alert_receiver", //ellipsis: true, align: "center", render: renderToolTip, }, alert_resolve: { title: "解决方案", key: "alert_resolve", dataIndex: "alert_resolve", //width: 100, //ellipsis: true, align: "center", render: formatTableRenderData, }, operating_system: { title: "操作系统", //width: 130, key: "operating_system", dataIndex: "operating_system", //ellipsis: true, align: "center", render: formatTableRenderData, }, alert_host_system: { title: "操作系统", //width: 130, key: "alert_host_system", dataIndex: "alert_host_system", //ellipsis: true, align: "center", render: formatTableRenderData, }, port: { title: "端口", //width: 100, key: "port", dataIndex: "port", //ellipsis: true, sorter: (a, b) => a.port - b.port, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, service_port: { title: "端口", //width: 100, key: "service_port", dataIndex: "service_port", //ellipsis: true, sorter: (a, b) => a.service_port - b.service_port, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, service_version: { title: "服务版本", //width: 120, key: "service_version", dataIndex: "service_version", //ellipsis: true, align: "center", render: formatTableRenderData, }, configuration_information: { title: "配置信息", //width: 120, key: "configuration_information", dataIndex: "configuration_information", //ellipsis: true, align: "center", render: renderInformation, }, cpu_rate: { title: "CPU使用率", width: 110, key: "cpu_rate", dataIndex: "cpu_rate", //ellipsis: true, sorter: (a, b) => tableSorter.sortUsageRate(a.cpu_rate, b.cpu_rate), render: (text, record, index) => { if (isTableTextInvalid(text)) return
-
; const _num = Number(Number(text).toFixed(2)); if (record.cpu_rate_check === "normal") { return ( ); } else if (record.cpu_rate_check === "critical") { return ( ); } else if (record.cpu_rate_check === "warning") { return ( ); } else { return ( ); } }, sortDirections: ["descend", "ascend"], align: "center", }, disk_rate: { title: "(根分区)使用率", width: 150, key: "disk_rate", dataIndex: "disk_rate", //ellipsis: true, sorter: (a, b) => tableSorter.sortUsageRate(a.disk_rate, b.disk_rate), render: (text, record, index) => { if (isTableTextInvalid(text)) return
-
; const _num = Number(Number(text).toFixed(2)); if (record.disk_rate_check === "normal") { return ( ); } else if (record.disk_rate_check === "critical") { return ( ); } else if (record.disk_rate_check === "warning") { return ( ); } else { return ( ); } }, sortDirections: ["descend", "ascend"], align: "center", }, disk_data_rate: { title: "(数据分区)使用率", width: 160, key: "disk_data_rate", dataIndex: "disk_data_rate", //ellipsis: true, sorter: (a, b) => tableSorter.sortUsageRate(a.disk_data_rate, b.disk_data_rate), render: (text, record, index) => { if (isTableTextInvalid(text)) return
-
; const _num = Number(Number(text).toFixed(2)); if (record.disk_data_check === "normal") { return ( ); } else if (record.disk_data_check === "critical") { return ( ); } else if (record.disk_data_check === "warning") { return ( ); } else { return ( ); } }, sortDirections: ["descend", "ascend"], align: "center", }, memory_rate: { title: "内存使用率", width: 100, key: "memory_rate", dataIndex: "memory_rate", //ellipsis: true, sorter: (a, b) => tableSorter.sortUsageRate(a.memory_rate, b.memory_rate), render: (text, record, index) => { if (isTableTextInvalid(text)) return
-
; const _num = Number(Number(text).toFixed(2)); if (record.memory_rate_check === "normal") { return ( ); } else if (record.memory_rate_check === "critical") { return ( ); } else if (record.memory_rate_check === "warning") { return ( ); } else { return ( ); } }, sortDirections: ["descend", "ascend"], align: "center", }, running_time: { title: "运行时间", key: "running_time", width: 120, dataIndex: "running_time", //ellipsis: true, sorter: (a, b) => Number(isTableTextInvalid(a.running_time) ? 0 : a.running_time) - Number(isTableTextInvalid(b.running_time) ? 0 : b.running_time), sortDirections: ["descend", "ascend"], align: "center", render: renderFormattedTime, }, ssh_state: { title: "SSH状态", width: 140, key: "ssh_state", dataIndex: "ssh_state", //ellipsis: true, align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 0) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}启用
); } else if (text === 1) { return
{renderCircular("#da4e48", "#fbe7e6")}禁用
; } else { return text; } }, }, agent_state: { title: "Agent状态", width: 140, key: "agent_state", dataIndex: "agent_state", //ellipsis: true, align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 0) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}正常
); } else if (text === 1) { return "安装中"; } else if (text === 2) { return "未安装"; } else if (text === 3) { return
{renderCircular("#da4e48", "#fbe7e6")}异常
; } else { return text; } }, }, cluster_name: { title: "集群名称", //width: 120, key: "cluster_name", dataIndex: "cluster_name", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.cluster_name); const str2 = R.defaultTo(" ", b.cluster_name); return ( str1.toLowerCase().charCodeAt(0) - str2.toLowerCase().charCodeAt(0) ); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, linkAddress: { title: "连接地址", //width: 150, key: "linkAddress", dataIndex: "linkAddress", //ellipsis: true, align: "center", render: formatTableRenderData, }, cluster_mode: { title: "集群模式", //width: 120, key: "cluster_mode", dataIndex: "cluster_mode", //ellipsis: true, align: "center", render: formatTableRenderData, }, quote: { title: "已引用", //width: 120, key: "quote", dataIndex: "quote", //ellipsis: true, align: "center", render: (text) => (text === 0 ? "否" : "是"), }, created_at: { title: "添加时间", //width: 120, key: "created_at", dataIndex: "created_at", //ellipsis: true, sorter: (a, b) => a - b, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, username: { title: "用户名", width: 120, key: "username", dataIndex: "username", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.username); const str2 = R.defaultTo(" ", b.username); return str1.charCodeAt(0) - str2.charCodeAt(0); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, role: { title: "角色", width: 120, key: "role", dataIndex: "role", //ellipsis: true, sorter: (a, b) => { const str1 = R.defaultTo(" ", a.role); const str2 = R.defaultTo(" ", b.role); return str1.charCodeAt(0) - str2.charCodeAt(0); }, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, login_time: { title: "登入时间", width: 180, key: "login_time", dataIndex: "login_time", //ellipsis: true, sorter: (a, b) => moment(a.login_time).valueOf() - moment(b.login_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, datetime: { title: "操作时间", width: 150, key: "datetime", dataIndex: "datetime", //ellipsis: true, sorter: (a, b) => moment(a.datetime).valueOf() - moment(b.datetime).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, status: { title: "用户状态", width: 160, key: "status", dataIndex: "status", //ellipsis: true, align: "center", render: formatTableRenderData, }, date_joined: { title: "创建时间", width: 120, key: "date_joined", dataIndex: "date_joined", //ellipsis: true, align: "center", render: formatTableRenderData, }, desc: { title: "描述", width: 220, key: "desc", dataIndex: "desc", //ellipsis: true, align: "center", render: formatTableRenderData, }, action: { title: "操作类型", width: 120, key: "action", dataIndex: "action", //ellipsis: true, align: "center", render: formatTableRenderData, }, permission_count: { title: "权限个数", width: 120, key: "permission_count", dataIndex: "permission_count", //ellipsis: true, align: "center", render: formatTableRenderData, }, // 巡检报告 inspection_operator: { title: "操作员", width: 80, key: "inspection_operator", dataIndex: "inspection_operator", //ellipsis: true, sorter: (a, b) => a.inspection_operator.charCodeAt(0) - b.inspection_operator.charCodeAt(0), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, inspection_status: { title: "巡检结果", width: 150, key: "inspection_status", dataIndex: "inspection_status", //ellipsis: true, sorter: (a, b) => a.inspection_status - b.inspection_status, sortDirections: ["descend", "ascend"], align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 2) { return
{renderCircular("#6cbe7b", "#e8f5eb")}成功
; } else if (text === 1) { return "进行中"; } else if (text === 0) { return "未开始"; } else if (text === 3) { return
{renderCircular("#da4e48", "#fbe7e6")}失败
; } else { return text; } }, }, run_status: { title: "执行结果", width: 120, key: "inspection_status", dataIndex: "inspection_status", //ellipsis: true, sorter: (a, b) => a.inspection_status - b.inspection_status, sortDirections: ["descend", "ascend"], align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 2) { return
{renderCircular("#6cbe7b", "#e8f5eb")}成功
; } else if (text === 1) { return "进行中"; } else if (text === 0) { return "未开始"; } else if (text === 3) { return
{renderCircular("#da4e48", "#fbe7e6")}失败
; } else { return text; } }, }, service_status: { title: "业务状态", //width: 120, key: "service_status", dataIndex: "service_status", //ellipsis: true, sorter: (a, b) => a.service_status - b.service_status, sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, // 产品管理-服务管理中新增的字段 product_service_status: { title: "运行状态", width: 120, key: "product_service_status", dataIndex: "service_status", //ellipsis: true, align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 0) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}未安装
); //return
运行
; } else if (text === 1) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}安装中
); } else if (text === 2) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}正常
); } else if (text === 3) { return
{renderCircular("#da4e48", "#fbe7e6")}异常
; } else if (text === 4) { return
{renderCircular("#da4e48", "#fbe7e6")}停止
; } else if (text == 5) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}启动中
); } else if (text == 6) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}停止中
); } else if (text == 7) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}重启中
); } else if (text == -1) { if (record.is_web_service) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}正常
); } else { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}未监控
); } } else { return text; } }, }, product_thrityPart_status: { title: "运行状态", //width: 100, key: "product_service_status", dataIndex: "state", //ellipsis: true, align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else if (text === 1) { return (
{renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}正常
); } else if (text === 2) { return (
{renderCircular("#f5c773", "rgba(247, 231, 24,.2)")}异常
); } else if (text === 0) { return
{renderCircular("#da4e48", "#fbe7e6")}停止
; } else { return text; } }, }, host_risk: { title: "主机风险", //width: 120, key: "host_risk", dataIndex: "host_risk", //ellipsis: true, sorter: (a, b) => a.host_risk - b.host_risk, sortDirections: ["descend", "ascend"], align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else { return `${text}个`; } }, }, service_risk: { title: "服务风险", //width: 120, key: "service_risk", dataIndex: "service_risk", //ellipsis: true, sorter: (a, b) => a.service_risk - b.service_risk, sortDirections: ["descend", "ascend"], align: "center", render: (text, record, index) => { if (isTableTextInvalid(text)) { return "-"; } else { return `${text}个`; } }, }, start_time: { title: "开始时间", width: 160, key: "start_time", dataIndex: "start_time", //ellipsis: true, sorter: (a, b) => moment(a.start_time).valueOf() - moment(b.start_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, patrol_start_time: { title: "开始时间", width: 200, key: "patrol_start_time", dataIndex: "start_time", //ellipsis: true, sorter: (a, b) => moment(a.start_time).valueOf() - moment(b.start_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, patrol_end_time: { title: "结束时间", width: 160, key: "patrol_end_time", dataIndex: "end_time", //ellipsis: true, sorter: (a, b) => moment(a.end_time).valueOf() - moment(b.end_time).valueOf(), sortDirections: ["descend", "ascend"], align: "center", render: formatTableRenderData, }, duration: { title: "用时", width: 100, key: "duration", dataIndex: "duration", //ellipsis: true, sorter: (a, b) => a.duration - b.duration, sortDirections: ["descend", "ascend"], align: "center", render: renderFormattedTime, }, // 巡检报告内容 // 主机风险 report_system: { title: "操作系统", //width: 150, key: "report_system", dataIndex: "system", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_risk_level: { title: "风险级别", key: "report_risk_level", dataIndex: "risk_level", //ellipsis: true, //width: 100, render: function renderFunc(text, record, index) { switch (record.risk_level) { case "critical": return ( ); case "warning": return ( ); default: return "-"; } }, align: "center", }, report_risk_describe: { width: 400, title: "风险描述", key: "report_risk_describe", dataIndex: "risk_describe", ellipsis: true, render: formatTableRenderData, align: "center", }, report_resolve_info: { title: "解决方案", key: "report_resolve_info", dataIndex: "resolve_info", //ellipsis: true, render: formatTableRenderData, align: "center", }, // 主机列表 report_release_version: { title: "操作系统", key: "report_release_version", dataIndex: "release_version", //ellipsis: true, //width: 180, render: formatTableRenderData, align: "center", }, report_host_massage: { title: "配置信息", key: "report_host_massage", dataIndex: "host_massage", //ellipsis: true, //ellipsis: true, //width: 180, render: formatTableRenderData, align: "center", }, report_disk_usage_root: { title: "根分区使用率", width: 150, key: "report_disk_usage_root", dataIndex: "disk_usage_root", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_disk_usage_data: { title: "数据分区使用率", width: 130, key: "report_disk_usage_data", dataIndex: "disk_usage_data", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_sys_load: { title: "平均负载", key: "report_sys_load", dataIndex: "sys_load", //width:180, //ellipsis: true, render: formatTableRenderData, align: "center", }, // 服务列表、数据库列表、组件列表 report_idx: { title: "序列", key: "index", width: 50, render: (text, record, index) => `${index + 1}`, align: "center", //fixed: "left", ////ellipsis:true }, report_host_ip: { title: "IP地址", key: "report_host_ip", dataIndex: "host_ip", //ellipsis: true, width: 150, render: formatTableRenderData, align: "center", }, report_log_level: { title: "日志等级", key: "report_log_level", dataIndex: "log_level", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_mem_usage: { title: "内存使用率", key: "report_mem_usage", dataIndex: "mem_usage", //ellipsis: true, width: 100, render: formatTableRenderData, align: "center", }, report_cpu_usage: { title: "CPU使用率", key: "report_cpu_usage", dataIndex: "cpu_usage", //ellipsis: true, width: 110, render: formatTableRenderData, align: "center", }, report_service_name: { title: "服务名称", //width: 200, key: "report_service_name", dataIndex: "service_name", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_service_port: { title: "端口号", key: "report_service_port", dataIndex: "service_port", //ellipsis: true, //width: 100, render: report_service_RenderData, align: "center", }, report_service_status: { title: "运行状态", key: "report_service_status", dataIndex: "service_status", //ellipsis: true, //width: 100, render: formatTableRenderData, align: "center", }, report_run_time: { title: "运行时间", width: 120, key: "report_run_time", dataIndex: "run_time", //ellipsis: true, render: formatTableRenderData, align: "center", }, report_cluster_name: { title: "集群名称", key: "report_cluster_name", dataIndex: "cluster_name", //ellipsis: true, render: formatTableRenderData, align: "center", }, operator: { title: "操作人员", key: "operator", dataIndex: "operator", align: "center", width: 80, }, install_process: { title: "安装进度", key: "install_process", dataIndex: "install_process", align: "center", width: 80, render: (text) => { if (text == "0%") { return {renderCircular("#da4e48", "#fbe7e6")}失败; } else if (text == "100%") { return ( {renderCircular("rgb(84, 187, 166)", "rgb(238, 250, 244)")}成功 ); } else { return text; } }, }, verson_start_time: { title: "开始时间", key: "start_time", dataIndex: "start_time", align: "center", width: 160, }, verson_end_time: { title: "结束时间", key: "end_time", dataIndex: "end_time", align: "center", width: 150, }, use_time: { title: "用时", key: "duration", dataIndex: "duration", render: (text) => { if (text && text !== "-") { let timer = moment.duration(text, "seconds"); let hours = timer.hours(); let hoursResult = hours ? `${hours}小时` : ""; let minutes = timer.minutes(); let minutesResult = minutes % 60 ? `${minutes % 60}分钟` : ""; let seconds = timer.seconds(); let secondsResult = seconds % 60 ? `${seconds % 60}秒` : ""; return `${hoursResult} ${minutesResult} ${secondsResult}`; // if(minutes >= 1){ // return `${minutes.toFixed()}分钟`; // }else{ // return `${text}秒`; // } } else { return "-"; } }, align: "center", width: 100, }, execution_mdoal: { title: "执行方式", align: "center", dataIndex: "execute_type", key: "execute_type", render: (text) => { if (text == "man") { return "手动执行"; } else if (text == "auto") { return "定时执行"; } else { return "-"; } }, width: 80, }, machine_idx: { title: "序列", key: "index", render: (text, record, index) => `${record._idx}`, align: "center", width: 60, //fixed: "left", }, /*eslint-disable*/ service_idx: { title: "序列", key: "index", dataIndex: "_idx", //render: (text, record, index) => `${index + 1}`, //ellipsis: true, align: "center", width: 60, //fixed: "left", // render:(text,record)=>{ // return ( //
// {record._idx} //
// ); // } }, /*eslint-disable*/ service_port_new: { title: "端口", width: 100, key: "service_port", dataIndex: "service_port", //ellipsis: true, sorter: (a, b) => a.service_port - b.service_port, sortDirections: ["descend", "ascend"], align: "center", render: (text) => { return text ? text : "-"; }, }, _port_new: { title: "端口", //width: 100, key: "port", dataIndex: "port", //ellipsis: true, sorter: (a, b) => a.port - b.port, sortDirections: ["descend", "ascend"], align: "center", render: (text) => { return text ? text : "-"; }, }, }; // 巡检报告-主机列表-连通性报告配置 export const host_port_connectivity_columns = [ { title: "服务", dataIndex: "name", //ellipsis: true, className: styles._bigfontSize, }, { title: "IP地址", dataIndex: "ip", //ellipsis: true, align: "center", className: styles._bigfontSize, }, { title: "端口", dataIndex: "port", //ellipsis: true, align: "center", className: styles._bigfontSize, }, /*eslint-disable*/ { title: "连通性", dataIndex: "status", //ellipsis: true, align: "center", className: styles._bigfontSize, render: (text) => { return (
{text}
); }, }, /*eslint-disable*/ ]; // 巡检报告-主机列表-内存使用率配置 export const host_memory_top_columns = [ { title: "TOP", dataIndex: "TOP", //ellipsis: true, width: 50, className: styles._bigfontSize, }, { title: "PID", dataIndex: "PID", //ellipsis: true, align: "center", width: 100, className: styles._bigfontSize, }, { title: "使用率", dataIndex: "P_RATE", //ellipsis: true, align: "center", width: 100, className: styles._bigfontSize, }, { title: "进程", dataIndex: "P_CMD", //ellipsis: true, className: styles._bigfontSize, }, ]; // 巡检报告-组件列表-kafka-分区信息 export const kafka_partition_columns = [ { title: "Topic", dataIndex: "topic", //ellipsis: true, className: styles._bigfontSize, }, { title: "分区数", dataIndex: "partition", //ellipsis: true, align: "center", className: styles._bigfontSize, }, { title: "副本数", dataIndex: "replication", //ellipsis: true, align: "center", className: styles._bigfontSize, }, ]; // 巡检报告-组件列表-kafka-消费位移信息 export const kafka_offsets_columns = [ { title: "Group", dataIndex: "group", //ellipsis: true, className: styles._bigfontSize, }, { title: "Topic", dataIndex: "topic", //ellipsis: true, className: styles._bigfontSize, }, { title: "Log Offset", dataIndex: "log_offset", //ellipsis: true, align: "center", className: styles._bigfontSize, }, { title: "Lag Offset", dataIndex: "lag_offset", //ellipsis: true, align: "center", className: styles._bigfontSize, }, ]; // 巡检报告-组件列表-kafka-topic消息大小 export const kafka_topic_size_columns = [ { title: "Topic", dataIndex: "topic", //ellipsis: true, className: styles._bigfontSize, }, { title: "Size", dataIndex: "size", //ellipsis: true, align: "center", className: styles._bigfontSize, }, ]; /** * table组件最后一列操作按钮的跳转逻辑 * 以=结尾的需要拼接ip地址 * @param record * @param type 默认是监控跳转 */ export const tableButtonHandler = (record, type = "monitor") => { if (type === "log") { if (isTableTextInvalid(record.monitor_log)) { return message.warn("请确认数据采集地址是否正确"); } //console.log(record.monitor_log,"===",record.service_name,record); if (record.service_name) { //window.open(`${record.monitor_log}${record.service_name}&var-env=${updata()().text}`); window.open(`${record.monitor_log}${record.service_name}`); } else if (record.alert_service_name) { //window.open(`${record.monitor_log}${record.alert_service_name}&var-env=${updata()().text}`); window.open(`${record.monitor_log}${record.alert_service_name}`); } } else { const url = record.monitor; if (isTableTextInvalid(url)) { return message.warn("请确认数据采集地址是否正确"); } else if (url.endsWith("=")) { // 主机管理、服务管理中用的ip,告警记录里用的alert_host_ip,但二者不会共存 // window.open(`${url}${record.ip ? record.ip : record.alert_host_ip}&var-env=${record.is_omp_host?record.master_env_name:updata()().text}`); window.open(`${url}${record.ip ? record.ip : record.alert_host_ip}`); } else if (url.endsWith("1")) { // 服务管理-自研服务,跳转监控时拼接服务名称 // window.open( // `${url}&var-app=${ // record.service_name ? record.service_name : record.alert_service_name // }&var-ip=${record.ip?record.ip:(record.alert_host_ip?record.alert_host_ip:undefined)}&var-env=${updata()().text}` // ); window.open( `${url}&var-app=${ record.service_name ? record.service_name : record.alert_service_name }&var-ip=${ record.ip ? record.ip : record.alert_host_ip ? record.alert_host_ip : undefined }` ); } else { //window.open(`${url}&var-env=${updata()().text}`); window.open(`${url}`); } } }; /** * 测试ip地址准确性 * @param ip * @returns {boolean} */ export function isValidIP(ip) { const reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\:([0-9]|[1-9]\d{1,3}|[1-5]\d{4}|6[0-5]{2}[0-3][0-5])$/; return reg.test(ip); } /** * @name: * @test: test font * @msg: * @param {*} data * @return {*} */ //给列表item添加idx做id用 export const _idxInit = (data) => { let result = [...data]; result.map((item, i) => { result[i]._idx = i + 1; result[i].key = result[i].id ? result[i].id : result[i]._idx; }); return result; }; export function TableRowButton({ buttonsArr }) { return (
{buttonsArr.map((item, idx) => { return (
item.btnHandler()}> {item.btnText}
); })}
); } export const refreshTime = () => { return getRefreshTimeChangeAction(moment().format("YYYY-MM-DD HH:mm:ss")); }; export function delCookie(name) { var exp = new Date(); exp.setTime(exp.getTime() - 1); var cval = getCookie(name); //console.log(cval) if (cval != null) document.cookie = name + "=" + cval + ';domin="localhost"' + ";expires=" + exp.toGMTString(); } export function getCookie(name) { //console.log(document.cookie) let arr = document.cookie.match(new RegExp("(^| )" + name + "=([^;]*)(;|$)")); if (arr != null) return unescape(arr[2]); return null; } export const logout = (login) => { delCookie("jwtToken"); localStorage.clear(); !login && window.__history__.replace("/login"); return; }; //文本非空处理 export const nonEmptyProcessing = (text) => { if (text === "" || text === null || text === undefined) { return "-"; } else { return `${text}`; } }; export const handleResponse = (res, succCallback, failedCallback) => { if (res.data.code === 0) { if (typeof succCallback === "function") { succCallback(res.data); } } if (res.data.code === 1) { if (res.data.message) { if (res.data.message == "未认证") { logout(); return; } message.warn(res.data.message); } if (typeof failedCallback === "function") { failedCallback(); } } }; export const colorConfig = { normal: "#76ca68", warning: "#ffbf00", critical: "#f04134", notMonitored: "rgb(170, 170, 170)", }; export const renderDisc = (level = "normal", size = 5, top = 0, left = 0) => { return (
); }; export const MessageTip = ({ setMsgShow, msgShow, msg }) => { return (
setMsgShow(false)} > {msg}
); }; //校验中文 export const isChineseChar = (str) => { var reg = /[\u4E00-\u9FA5\uF900-\uFA2D]/; return reg.test(str); }; //校验数字 export const isNumberChar = (str) => { const reg = /^\d+$/; return reg.test(str); }; // 校验小写 export const isLowercaseChar = (str) => { const reg = /^[a-z]+$/; return reg.test(str); }; // 校验大写 export const isUppercaseChar = (str) => { const reg = /^[A-Z]+$/; return reg.test(str); }; // 校验字母 export const isLetterChar = (str) => { const reg = /^[a-zA-Z]+$/; return reg.test(str); }; // 校验ip export const isValidIpChar = (ip) => { var reg = /^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$/; return reg.test(ip); }; // 校验表情 export const isExpression = (str) => { var reg = /[^\u0020-\u007E\u00A0-\u00BE\u2E80-\uA4CF\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFFEF\u0080-\u009F\u2000-\u201f\u2026\u2022\u20ac\r\n]/g; return reg.test(str); }; // 校验空格 export const isSpace = (str) => { return str.includes(" "); }; export function debounce(fn, wait) { return function () { clearTimeout(window.timer); window.timer = setTimeout(fn, wait); }; } // 校验密码 export function isPassword(str) { var reg = /[^a-zA-Z0-9\`\~\!\?\@\#\$\%\^\&\,\(\)\[\]\{\}\_\+\_\*\/\.\;\:]/g; return reg.test(str); } // 下载文件 export const downloadFile = (url) => { let a = document.createElement("a"); a.href = url; a.download = url.split("/").pop(); document.body.appendChild(a); a.click(); document.body.removeChild(a); }; // 检测对象类型 function checkType(any) { return Object.prototype.toString.call(any).slice(8, -1); } // 深拷贝函数 export const clone = (any) => { if (checkType(any) === "Object") { // 拷贝对象 let o = {}; for (let key in any) { o[key] = clone(any[key]); } return o; } else if (checkType(any) === "Array") { // 拷贝数组 var arr = []; for (let i = 0, leng = any.length; i < leng; i++) { arr[i] = clone(any[i]); } return arr; } else if (checkType(any) === "Function") { // 拷贝函数 return new Function("return " + any.toString()).call(this); } else if (checkType(any) === "Date") { // 拷贝日期 return new Date(any.valueOf()); } else if (checkType(any) === "RegExp") { // 拷贝正则 return new RegExp(any); } else if (checkType(any) === "Map") { // 拷贝Map 集合 let m = new Map(); any.forEach((v, k) => { m.set(k, clone(v)); }); return m; } else if (checkType(any) === "Set") { // 拷贝Set 集合 let s = new Set(); for (let val of any.values()) { s.add(clone(val)); } return s; } return any; }; export const randomNumber = (length = 6) => { let r = ""; let str = "QWERTYUIOPLKJHGFDSAZXCVBNM123456790"; new Array(length).fill(0).map((item) => { let num = parseInt(Math.random() * 26); r += str[num]; }); return r; }; //定义加密函数 export const encrypt = (message) => { var encrypt = new JSEncrypt(); encrypt.setPublicKey(PublicKey); // publicKey为公钥 const txt = encrypt.encrypt(message); return txt; }; // 根据result渲染状态 export const RenderStatusForResult = ({ result }) => { if (result === "success") { return ( ); } else { return ( ); } }; ================================================ FILE: omp_web/tsconfig.json ================================================ { "compilerOptions": { "allowUnreachableCode": true, "allowUnusedLabels": false, "alwaysStrict": false, "baseUrl": ".", "experimentalDecorators": true, "jsx": "react-jsx", "sourceMap": true, "module": "esnext", "noImplicitAny": false, "removeComments": true, "types": [ "node" ], "target": "ESNext", "outDir": "./dist", "declaration": true, "declarationDir": "./lib", "allowJs": true, "lib": [ "es5", "es2015", "es2016", "es2017", "es2018", "dom" ], "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": [ "src" ] } ================================================ FILE: package_hub/.gitkeep ================================================ ================================================ FILE: package_hub/_modules/__init__.py ================================================ # -*- coding: utf-8 -*- # Project: __init__.py # Author: jon.liu@yunzhihui.com # Create time: 2021-09-24 12:01 # IDE: PyCharm # Version: 1.0 # Introduction: ================================================ FILE: package_hub/_modules/arangodb_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get arangodb Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP, GetProcess_ServiceMem def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'arangodb': pid = pnum return pid except Exception: pass except Exception: return None # def GetProcess_LogLevel(pid): # log_level = None # if pid and type(pid).__name__ == 'int': # try: # p = psutil.Process(pid) # arangodb_path = p.cmdline() # print(arangodb_path[2]) # f = open('%s' % (arangodb_path[2]), 'r') # for lines_list in f: # if 'level =' in lines_list: # arangodb_log_level = lines_list.strip('\n').split() # log_level = arangodb_log_level[-1] # return log_level # except Exception: # return None # else: # return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = "INFO" process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="arangodb") return json.dumps(process_message) if __name__ == "__main__": print(main()) ================================================ FILE: package_hub/_modules/beanstalkd_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get beanstalkd Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP, GetProcess_ServiceMem def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'beanstalkd': pid = pnum return pid except Exception: pass except Exception: return None # def GetProcess_LogLevel(pid): # log_level = None # if pid and type(pid).__name__ == 'int': # try: # p = psutil.Process(pid) # beanstalkd_path = p.cmdline() # print(beanstalkd_path[2]) # f = open('%s' % (beanstalkd_path[2]), 'r') # for lines_list in f: # if 'level =' in lines_list: # beanstalkd_log_level = lines_list.strip('\n').split() # log_level = beanstalkd_log_level[-1] # return log_level # except Exception: # return None # else: # return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = "INFO" process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="beanstalkd") return json.dumps(process_message) if __name__ == "__main__": print(main()) ================================================ FILE: package_hub/_modules/clickhouse_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get clickhouse Inspection data import json import os import time import os.path as up import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre def GetClickhouse_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'clickhouse-server': pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_ServiceMem(pid): if pid and type(pid).__name__ == 'int': try: service_mem_list = list() p = psutil.Process(pid) ch_path = up.abspath(up.join(p.exe(), "../..")) f = open('%s/etc/clickhouse-server/users.xml' % (ch_path), 'r') for lines_list in f: if '' in lines_list: service_mem_list = lines_list.strip().replace( '>', '').replace('' in lines_list: service_log_level = lines_list.strip().replace( '>', '').replace('/dev/null' % ( data_path, data_path, data_path) distribute_size_list = os.popen(cmd).read().strip( '\n').replace('\t', ' ').split('\n') distribute_size = distribute_size_list[-1].split() size = distribute_size[0] return size else: return None def GetRealTime_Data(pid, host, port, user, password): realtime_json = {} if pid and type(pid).__name__ == 'int': tsb_table_list = ['host_basic_all', 'browser_page_all', 'app_request_all', 'mobile_basic_all'] now_time = int(time.time()) ago_tiem = int(now_time) - 300 p = psutil.Process(pid) ch_path = up.abspath(up.join(p.exe(), "../..")) if password: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -u %s --password %s -q ' % ( ch_path, host, port, user, password) else: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -q' % ( ch_path, host, port) for table in tsb_table_list: cmd = ck_bin + '"select count() from tsb_distribute.%s where current_time between toDateTime(%d) and toDateTime(%d);"' % ( table, ago_tiem, now_time) + " 2>/dev/null" realtime = os.popen(cmd).read().strip('\n') try: if int(realtime) > 0: realtime_json[table] = 'True' else: realtime_json[table] = 'False' except Exception: realtime_json[table] = 'False' return realtime_json else: return None def GetJKBRealTime_Data(pid, host, port, user, password): realtime_json = {} if pid and type(pid).__name__ == 'int': table_list = ['api_snapshot_data_all', 'task_snapshot_all'] now_time = int(time.time()) ago_tiem = int(now_time) - 300 p = psutil.Process(pid) ch_path = up.abspath(up.join(p.exe(), "../..")) if password: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -u %s --password %s -q ' % ( ch_path, host, port, user, password) else: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -q' % ( ch_path, host, port) for table in table_list: cmd = ck_bin + '"select count() from jkb_distribute.%s where current_time between toDateTime(%d) and toDateTime(%d);"' % ( table, ago_tiem, now_time) + " 2>/dev/null" realtime = os.popen(cmd).read().strip('\n') try: if int(realtime) > 0: realtime_json[table] = 'True' else: realtime_json[table] = 'False' except Exception: realtime_json[table] = 'False' return realtime_json else: return None def GetCluster_IP(pid, host, port, user, password): if pid and type(pid).__name__ == 'int': p = psutil.Process(pid) ch_path = up.abspath(up.join(p.exe(), "../..")) if password: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -u %s --password %s -q ' % ( ch_path, host, port, user, password) else: ck_bin = '%s/bin/clickhouse-client -m -h %s --port %s -q' % ( ch_path, host, port) try: cmd = ck_bin + '''"select host_address from system.clusters where host_address not like '127.0.0.1' group by host_address;"''' cluster_ip = os.popen(cmd).read().strip('\n').split('\n') return cluster_ip except Exception: return None def main(pid=GetClickhouse_Pid(), host='127.0.0.1', port='18101', user='default', password='', **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["cluser_ip"] = GetCluster_IP( pid, host, port, user, password) process_message["table_readonly"] = GetTable_Readonly( pid, host, port, user, password) process_message["nodedata_size"] = GetNodeData_Size( pid, host, port, user, password) process_message["distribute_size"] = GetDistribute_Size(pid) process_message["tsb_realtime"] = GetRealTime_Data( pid, host, port, user, password) process_message["jkb_realtime"] = GetJKBRealTime_Data( pid, host, port, user, password) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/elasticsearch_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get es Inspection data import json import ssl import urllib.request import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'java' and 'elasticsearch' in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): log_level = None if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) es_path = p.cwd() f = open('%s/config/log4j2.properties' % (es_path), 'r') for lines_list in f: if 'logger.action.level =' in lines_list: es_log_level = lines_list.strip('\n').split() log_level = es_log_level[-1] except Exception: log_level = None return log_level else: return None def GetCluster_Status(port): url = 'http://127.0.0.1' + ':' + str(port) + "/_cluster/health" ssl._create_default_https_context = ssl._create_unverified_context try: cluster_list = urllib.request.urlopen('%s' % (url), timeout=15) cluster_line = json.loads(cluster_list.read()) cluster = cluster_line["status"] return cluster except Exception: return None def GetIndex_Status(port): status = {} url = 'http://127.0.0.1' + ':' + str(port) + "/_cat/indices" ssl._create_default_https_context = ssl._create_unverified_context try: index_list = urllib.request.urlopen('%s' % (url), timeout=15) index_line = index_list.read().decode().strip().split('\n') for index in index_line: index_status = index.split() if index_status[0] != 'green' and index_status[1] == 'open': status[index_status[2]] = index_status[0] if status: return status else: return 'green' except Exception: return None def main(pid=GetProcess_Pid(), port=18115, json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid, is_java=True) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message['cluster_status'] = GetCluster_Status(port) process_message["index_status"] = GetIndex_Status(port) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="elasticsearch") return json.dumps(process_message) if __name__ == "__main__": print(main()) ================================================ FILE: package_hub/_modules/flink_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get flink Inspection data import json import os import socket import time import psutil def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'java' and "flink" in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid): if pid and type(pid).__name__ == 'int': return "True" else: return "False" def GetProcess_Port(pid): try: if pid and type(pid).__name__ == 'int': port = [] # p = psutil.Process(pid) cmd = 'ss -tnlp | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[3].split(':') port.append(port_aa[-1]) port = list(set(port)) return port else: return None except Exception: return None def GetProcess_Runtime(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,etime|grep ' + str(pid) etime = os.popen(cmd).read().strip('\n').split() if '-' in etime[1]: runtime = etime[1].replace('-', ' day ') else: runtime = etime[1] except Exception: runtime = None else: runtime = None return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid): if pid and type(pid).__name__ == 'int': try: pid_cpuinfo = {} p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] cpu_usage = "{:.2f}".format(cpus_percent) + "%" except Exception: cpu_usage = None else: cpu_usage = None return cpu_usage def GetProcess_Mem(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) process_mem = p.memory_percent() mem_usage = "{:.2f}".format(process_mem) + "%" except Exception: return None else: mem_usage = None return mem_usage def GetProcess_ServiceMem(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,command|grep %s' % (pid) process_list = os.popen(cmd).read().strip('\n').split('-Xms') process_mem = process_list[-1].split() service_mem = process_mem[0] return service_mem except Exception: return None else: return None # def GetProcess_LogLevel(pid): # if pid and type(pid).__name__ == 'int': # try: # p = psutil.Process(pid) # domm_path = p.cwd() # f = open('%s/conf/log4j2.xml' % (domm_path),'r') # for lines_list in f: # if '','') # except Exception: # log_level = None # return log_level # else: # return None def main(pid=GetProcess_Pid(), **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = None process_message["cluster_ip"] = None return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/get_agent_info.py ================================================ # -*- coding: utf-8 -*- # Project: get_agent_info # Author: jon.liu@yunzhihui.com # Create time: 2020-12-31 19:04 # IDE: PyCharm # Version: 1.0 # Introduction: import math import socket import psutil import salt.utils.network def byte_to_gb(b): """ byte 转 gb :param b: :return: """ if not isinstance(b, int): return 0 return math.ceil(b / 1024 / 1024 / 1024) def get_cpu_info(): """ 获取cpu个数信息 :return: """ return psutil.cpu_count() def get_memory_detail(): """ 获取内存使用信息 :return: """ memory = psutil.virtual_memory() memory_total = int(memory.total) memory_used = int(memory.used) memory_free = int(memory.free) memory_available = int(memory.available) return { "memory_total": byte_to_gb(memory_total), "memory_used": byte_to_gb(memory_used), "memory_free": byte_to_gb(memory_free), "memory_available": byte_to_gb(memory_available) } def get_disk_detail(): """ 获取磁盘使用信息 :return: """ all_partitions = psutil.disk_partitions() ret_dic = {} for item in all_partitions: # 当过滤到挂载盘中有以下关键字时,跳过此磁盘的检查 if "docker/overlay" in item.mountpoint or \ "docker/container" in item.mountpoint or \ "/boot" == item.mountpoint or \ item.mountpoint.startswith("/run/media"): continue disk_usage = psutil.disk_usage(item.mountpoint) _disk_total = byte_to_gb(int(disk_usage.total)) ret_dic[item.mountpoint] = _disk_total return ret_dic def get_hostname(): """ 获取主机名信息 :return: """ return socket.gethostname() def get_ip(): """ 获取ip地址信息 :return: """ all_ips = salt.utils.network.ip_addrs() for item in all_ips: if item.startswith("127"): continue return item def get_agent_info(): """ 获取agent信息 :return: """ return { "cpu": get_cpu_info(), "disk": get_disk_detail(), "memory": get_memory_detail(), "hostname": get_hostname(), "ip": get_ip() } if __name__ == '__main__': print(get_agent_info()) ================================================ FILE: package_hub/_modules/gotty_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get gotty Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'gotty' and 'gotty' in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = None process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="gotty") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/grafana_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get grafana Inspection data import os import re import psutil import time import json import socket import os.path as up def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'grafana-server': pid = pnum return pid except Exception: pass except Exception: return None def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid): if pid and type(pid).__name__ == 'int': return "True" else: return "False" def GetProcess_Port(pid): try: if pid and type(pid).__name__ == 'int': port = [] # p = psutil.Process(pid) cmd = 'ss -tnlp | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[3].split(':') port.append(port_aa[-1]) port = list(set(port)) return port else: return None except Exception: return None def GetProcess_Runtime(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,etime|grep ' + str(pid) etime = os.popen(cmd).read().strip('\n').split() if '-' in etime[1]: runtime = etime[1].replace('-', ' day ') else: runtime = etime[1] except Exception: runtime = None else: runtime = None return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid): try: pid_cpuinfo = {} p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] cpu_usage = "{:.2f}".format(cpus_percent) + "%" except Exception: cpu_usage = None return cpu_usage def GetProcess_Mem(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) process_mem = p.memory_percent() mem_usage = "{:.2f}".format(process_mem) + "%" except Exception: return None else: mem_usage = None return mem_usage def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) ignite_path = p.cwd() f = open('%s/conf/defaults.ini' % (ignite_path), 'r') for lines_list in f: match = re.search(r'level = ([a-z]+)', lines_list) if match: ignite_log_level = match.group(0).split() log_level = ignite_log_level[-1] return log_level except Exception: return None else: return None def GetCluster_IP(pid): if pid and type(pid).__name__ == 'int': try: cluster_ip = [] p = psutil.Process(pid) grafana_path = up.abspath(up.join(p.cwd(), "..")) cmd = 'grep grafana %s/task.list' % (grafana_path) cluster_list = os.popen(cmd).read().strip('\n').split('\n') if int(len(cluster_list)) > 1: for cluster_line in cluster_list: cluster = cluster_line.split() cluster_ip.append(cluster[0]) else: cluster_ip = None return cluster_ip except Exception: return None else: return None def main(pid=GetProcess_Pid(), **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP(pid) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/hadoop_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get domm Inspection data import json import os import socket import time import psutil def GetProcess_Pid(): try: cmd = "ps -eo pid,command |grep 'Dhadoop' | grep -v grep" cmd_list = os.popen(cmd).read().strip('\n').split() pid = int(cmd_list[0]) return pid except Exception: return None def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid): if pid and type(pid).__name__ == 'int': return "True" else: return "False" def GetProcess_Port(pid): try: if pid and type(pid).__name__ == 'int': port = [] # p = psutil.Process(pid) cmd = 'ss -tnlp | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[3].split(':') port.append(port_aa[-1]) port = list(set(port)) return port else: return None except Exception: return None def GetProcess_Runtime(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,etime|grep ' + str(pid) etime = os.popen(cmd).read().strip('\n').split() if '-' in etime[1]: runtime = etime[1].replace('-', ' day ') else: runtime = etime[1] except Exception: runtime = None else: runtime = None return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid): if pid and type(pid).__name__ == 'int': try: pid_cpuinfo = {} p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] cpu_usage = "{:.2f}".format(cpus_percent) + "%" except Exception: cpu_usage = None else: cpu_usage = None return cpu_usage def GetProcess_Mem(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) process_mem = p.memory_percent() mem_usage = "{:.2f}".format(process_mem) + "%" except Exception: return None else: mem_usage = None return mem_usage def GetProcess_ServiceMem(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,command|grep %s' % (pid) process_list = os.popen(cmd).read().strip('\n').split('-Xms') process_mem = process_list[-1].split() service_mem = process_mem[0] return service_mem except Exception: return None else: return None # def GetProcess_LogLevel(pid): # if pid and type(pid).__name__ == 'int': # try: # p = psutil.Process(pid) # domm_path = p.cwd() # f = open('%s/conf/log4j2.xml' % (domm_path),'r') # for lines_list in f: # if '','') # except Exception: # log_level = None # return log_level # else: # return None def main(pid=GetProcess_Pid(), **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = None process_message["cluster_ip"] = None return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/host_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get host Inspection data import datetime import json import os import platform import re import socket import subprocess import time import psutil def run_cmd(cmd): """ 运行系统命令,返回标准输出,标准错误输出及执行状态码 :param cmd: :return: """ p = subprocess.run( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd_stdout = bytes.decode(p.stdout) if cmd_stdout.endswith('\n'): cmd_stdout = cmd_stdout.strip() # cmd_stderr = bytes.decode(p.stderr) if p.returncode == '0': return None else: # return cmd_stdout, cmd_stderr, p.returncode return cmd_stdout def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetHostname_Info(): try: host_name = socket.gethostname() return host_name except Exception: return None def GetRelease_Version(): try: if os.path.exists('/etc/redhat-release'): with open('/etc/redhat-release') as file: for line in file: return line.strip('\n') else: return None except Exception: return None def GetKernel_Version(): """ 获取系统内核版本 :return: """ try: release_version_list = platform.platform() release_version_line = release_version_list.split('-with-') release_version = release_version_line[0] return release_version except Exception: return None def GetSelinux_Status(): """ 获取selinux状态 :return: """ try: cmd = 'getenforce' selinux_status = run_cmd(cmd) return selinux_status except Exception: return None def GetUmask_Status(): try: user_cmd = 'whoami' user = run_cmd(user_cmd) cmd = 'umask' umask_status = run_cmd(cmd) return {"user": user, "umask": umask_status} except Exception: return None def GetUlimit_Num(): try: cmd = 'ulimit -n' ulimit_num = run_cmd(cmd) return ulimit_num except Exception: return None def GetTimeNow_Info(): """ 获取系统当前时间 :return: """ try: time_now = datetime.datetime.strftime( datetime.datetime.now(), '%Y-%m-%d %H:%M:%S') return time_now except Exception: return None def GetRunTime_Info(): """ 获取系统运行时间 :return: """ try: cmd = "uptime" uptime_list = run_cmd(cmd).strip().split('up') run_time = uptime_list[1].strip().split(',') if 'user' in run_time[1]: time = run_time[0] else: time = run_time[0] + run_time[1] return time except Exception: return None def GetCpu_Total(): try: cpu_count = str(psutil.cpu_count()) + "C" return cpu_count except Exception: return None def GetMemory_Total(): try: with open('/proc/meminfo') as fd: for line in fd: if line.startswith('MemTotal'): mem = int(line.split()[1].strip()) break mem = int('%.f' % (mem / 1024.0)) if mem > 1024: mem = '%.f' % (mem / 1024.0) + 'GB' else: mem = str(mem) + 'MB' return mem except Exception: return None def GetDisk_Total(): try: total_mb = 0 df_cmd = "df -h | grep -v 'tmpfs' | tail -n +2" result = run_cmd(df_cmd).strip().split('\n') for total_list in result: total = total_list.split() if 'T' in total[1]: total_tb = total[1].replace('T', '') total_mb += int('%.f' % (int(float(total_tb)) * 1024.0 * 1024.0)) if 'G' in total[1]: total_gb = total[1].replace('G', '') total_mb += int('%.f' % (int(float(total_gb)) * 1024.0)) if 'M' in total[1]: total_mb += int(float(total[1].replace('M', ''))) if total_mb > 1024 and total_mb < 1048576: disk_total = '%.f' % (total_mb / 1024.0) + 'GB' elif total_mb > 1048576: disk_total = '%.f' % (total_mb / 1024.0 / 1024.0) + 'TB' else: disk_total = str(total_mb) + 'MB' return disk_total except Exception: return None def GetMemory_Usage(): """ 获取内存使用信息 :return: """ try: svmem = psutil.virtual_memory() mem_usage = str(svmem.percent) + '%' return mem_usage except Exception: return None def GetCpu_Usage(): """ 获取cpu使用率 :return: """ try: cpu_usage = str(psutil.cpu_percent()) + '%' return cpu_usage except Exception: return None def GetDisk_Info(data_path): """ 获取磁盘使用量信息 :return: """ try: disk_usage_json = {} df_cmd = "df -h | grep -v 'tmpfs' | tail -n +2" result = run_cmd(df_cmd).strip().split('\n') for total_list in result: total = total_list.split() if total[-1] == '/': disk_usage_json[total[-1]] = total[-2] if total[-1] == data_path: disk_usage_json[total[-1]] = total[-2] return disk_usage_json except Exception: return None def GetInode_Info(data_path): """ 获取inode使用量信息 :return: """ try: inode_usage_json = {} inode_cmd = "df -i | grep -v 'tmpfs' | tail -n +2" result = run_cmd(inode_cmd).strip().split('\n') for total_list in result: total = total_list.split() if total[-1] == '/': inode_usage_json[total[-1]] = total[-2] if total[-1] == data_path: inode_usage_json[total[-1]] = total[-2] return inode_usage_json except Exception: return None def GetSysLoad_Average(): """ 获取系统平均负载信息 :return: """ try: load_average_info = psutil.getloadavg() load_average_dict = dict() load_average_dict.update({'m1_average_load': load_average_info[0]}) load_average_dict.update({'m5_average_load': load_average_info[1]}) load_average_dict.update({'m15_average_load': load_average_info[2]}) load_average_json = load_average_dict return load_average_json except Exception: return None def GetServicesPort_Connectivity(port_list): try: port_json = [] for port in port_list: sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.settimeout(2) status = sk.connect_ex((port['ip'], int(port['port']))) if status != 0: port["status"] = "False" port_json.append(port) if len(port_json) > 0: return port_json else: return 'Ture' except socket.error: return None sk.close() def GetServer_Bandwidth(): try: bandwidth_1 = psutil.net_io_counters() time.sleep(1) bandwidth_2 = psutil.net_io_counters() bandwidth_sent = '%.f' % ( (bandwidth_2.bytes_sent - bandwidth_1.bytes_sent) / 1024) bandwidth_recv = '%.f' % ( (bandwidth_2.bytes_recv - bandwidth_1.bytes_recv) / 1024) sent = bandwidth_sent + "KB/s" receive = bandwidth_recv + "KB/s" return {"sent": sent, "receive": receive} except Exception: return None def GetDisK_ReadWrite(): try: disk_1 = psutil.disk_io_counters() time.sleep(1) disk_2 = psutil.disk_io_counters() disk_read = '%.f' % ((disk_2.read_bytes - disk_1.read_bytes) / 1024) disk_write = '%.f' % ((disk_2.write_bytes - disk_1.write_bytes) / 1024) read = disk_read + "KB/s" write = disk_write + "KB/s" return {"read": read, "write": write} except Exception: return None def GetDisk_IoWait(): try: disk_iowait = 0 cmd = "vmstat 1 10" iowait_list = run_cmd(cmd).strip().split('\n') for iowait in iowait_list: if 'cpu' in iowait_list or 'wa' in iowait: continue else: disk_iowait += int(iowait.split()[-2]) disk = disk_iowait / 10 return disk except Exception: return None def GetMemory_Top10(): """ 获取占用内存前10的应用 :return: """ class Cwp: def __init__(self, pid, memory_percent, cmdline): self.pid = pid self.cmdline = cmdline self.memory_percent = memory_percent all_pids = psutil.pids() cw_ps = [] for ele in all_pids: try: p = psutil.Process(ele) cw_p = Cwp(p.pid, p.memory_percent(), ' '.join(p.cmdline())) cw_ps.append(cw_p) except psutil.Error: continue cw_ps.sort(key=lambda c: c.memory_percent, reverse=True) content_list = list() for ele in cw_ps[:10]: tma_dict = { 'TOP': str(cw_ps.index(ele) + 1), 'PID': ele.pid, 'P_RATE': str(round(ele.memory_percent, 2)) + '%', 'P_CMD': ele.cmdline } content_list.append(tma_dict) top10_mem_app_json = content_list return top10_mem_app_json def GetCpu_Top10(): """ 获取cpu使用率前10的应用 :return: """ class Cwp: def __init__(self, pid, cpu_percent, cmdline): self.pid = pid self.cmdline = cmdline self.cpu_percent = cpu_percent all_pids = psutil.pids() cw_ps = [] for ele in all_pids: try: p = psutil.Process(ele) if len(p.cmdline()) >= 1: cpu_p = p.cpu_percent(interval=1) cw_p = Cwp(p.pid, cpu_p, ' '.join(p.cmdline())) cw_ps.append(cw_p) except psutil.Error: continue cw_ps.sort(key=lambda c: c.cpu_percent, reverse=True) content_list = list() for ele in cw_ps[:10]: tca_dict = { 'TOP': str(cw_ps.index(ele) + 1), 'PID': ele.pid, 'P_RATE': str(round(ele.cpu_percent, 2)) + '%', 'P_CMD': ele.cmdline } content_list.append(tca_dict) top10_cpu_app_json = content_list return top10_cpu_app_json def GetKernel_Info(): try: cmd = "egrep -v '^#|^$' /etc/sysctl.conf" sysctl = run_cmd(cmd).strip().split('\n') if len(sysctl) > 0: return sysctl else: return None except Exception: return None def GetBoot_Start(): try: boot_start = [] release_version_list = platform.platform() if '-6.' in release_version_list: cmd = "chkconfig --list" if '-7.' in release_version_list: cmd = 'systemctl list-unit-files | grep enabled' boot_start_list = run_cmd(cmd).strip().split('\n') for line_list in boot_start_list: line = line_list.split() boot_start.append(line[0]) return boot_start except Exception: return None def GetZombies_Status(): try: cmd = "ps -A -ostat,ppid,cmd |grep -e '^[Zz]'" zombies_status_list = run_cmd(cmd).strip().split('\n') if len(zombies_status_list) > 0: return zombies_status_list else: return None except Exception: return None def GatRun_Process(): """ 获取正在运行的进程数 :return: """ try: all_process_num = len(psutil.pids()) return all_process_num except Exception: return None def GetFirewall_Info(): try: get_firewall_cmd = "iptables -nL" result = run_cmd(get_firewall_cmd) new_str = "".join([s for s in result.splitlines(True) if s.strip()]) if len(new_str.split('\n')) == 6: return None fw_block_list = result.split('\n\n') firewall_dict = dict() for block in fw_block_list: block_rules_list = list() tmp_rules = block.split('\n') fw_title = tmp_rules[0] for line in tmp_rules: rule_dict = dict() if line.startswith('Chain') or line.startswith('target'): continue items = re.split(r'\s+', line) rule_dict.update({'target': items[0]}) rule_dict.update({'port': items[1]}) rule_dict.update({'opt': items[2]}) rule_dict.update({'source': items[3]}) rule_dict.update({'destination': items[4]}) rule_dict.update({'others': ' '.join(items[5:])}) block_rules_list.append(rule_dict) if len(block_rules_list) > 0: firewall_dict.update({fw_title: block_rules_list}) firewall_json = firewall_dict return firewall_json except Exception: return None def main(data_path='/data', port_list=[{"name": "ssh", "ip": "127.0.0.1", "port": "36000"}], **kwargs): process_message = dict() # process_message["IP"] = GetLocal_Ip() # process_message["hostname"] = GetHostname_Info() process_message["release_version"] = GetRelease_Version() process_message["kernel_version"] = GetKernel_Version() process_message["selinux"] = GetSelinux_Status() process_message["umask"] = GetUmask_Status() # process_message["max_openfile"] = GetUlimit_Num() # process_message["now_time"] = GetTimeNow_Info() # process_message["run_time"] = GetRunTime_Info() # process_message["host_massage"] = {"cpu": GetCpu_Total(), "memory": GetMemory_Total(), "disk": GetDisk_Total()} # process_message["memory_usage"] = GetMemory_Usage() # process_message["cpu_usage"] = GetCpu_Usage() # process_message["disk_usage"] = GetDisk_Info(data_path) # process_message["inode_usage"] = GetInode_Info(data_path) # process_message["sys_load"] = GetSysLoad_Average() # process_message["port_connectivity"] = GetServicesPort_Connectivity(port_list) # process_message["bandwidth"] = GetServer_Bandwidth() # process_message["throughput"] = GetDisK_ReadWrite() # process_message["iowait"] = GetDisk_IoWait() process_message["memory_top"] = GetMemory_Top10() process_message["cpu_top"] = GetCpu_Top10() process_message["kernel_parameters"] = GetKernel_Info() # process_message["boot_start"] = GetBoot_Start() process_message["zombies_process"] = GetZombies_Status() process_message["run_process"] = GatRun_Process() # process_message["iptables"] = GetFirewall_Info() return json.dumps(process_message) ================================================ FILE: package_hub/_modules/httpd_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get httpd Inspection data import json import os.path as up import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'httpd' and 'httpd/bin/httpd' in p.exe(): pid = int(p.ppid()) if pid != 1: return pid else: return pnum except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: log_level = '' p = psutil.Process(pid) httpd_path = up.abspath(up.join(p.exe(), "../..")) f = open('%s/conf/httpd.conf' % (httpd_path), 'r') for lines_list in f: if 'access_' in lines_list: log_level = 'access' return log_level except Exception: return None else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="httpd") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/ignite_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get ignite Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'java' and 'ignite' in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): log_level = None if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) ignite_path = p.cwd() f = open('%s/config/ignite-log4j.xml' % (ignite_path), 'r') for lines_list in f: if '' in lines_list: ignite_log_level = lines_list.strip('\n').split('"') log_level = ignite_log_level[1] return log_level except Exception: return None else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid, is_java=True) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="ignite") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/init_host.py ================================================ #!/usr/bin/env python # -*- coding: utf-8 -*- import json import os import sys import logging import time import logging.config import subprocess PYTHON_VERSION = sys.version_info.major CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) if PYTHON_VERSION == 2: reload(sys) sys.setdefaultencoding('utf-8') # ---- 日志定义部分 ---- # 日志配置 建议输入绝对路径,默认生成日志会添加 时间字段 LOG_PATH = "/tmp/init_host_standalone.log" # 屏幕输出日志级别 CONSOLE_LOG_LEVEL = logging.INFO # 文件日志级别 FILE_LOG_LEVEL = logging.DEBUG def generate_log_filepath(log_path): """生成日志名称""" time_str = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) dirname = os.path.dirname(log_path) name_split = os.path.basename(log_path).split('.') if len(name_split) == 1: name = "{0}_{1}".format(name_split[0], time_str) file_path = os.path.join(dirname, name) else: name_split.insert(-1, time_str) file_path = os.path.join(dirname, '.'.join(name_split)) return file_path # 日志配置 log_path = generate_log_filepath(LOG_PATH) logging.basicConfig( level=logging.DEBUG, format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', filename=log_path, filemode='a') console = logging.StreamHandler() console.setLevel(logging.INFO) formatter = logging.Formatter( '%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s') console.setFormatter(formatter) logger = logging.getLogger() logger.addHandler(console) KERNEL_PARAM = """# Disable IPv6 net.ipv6.conf.all.disable_ipv6 = 1 net.ipv6.conf.default.disable_ipv6 = 1 # ARP net.ipv4.conf.default.rp_filter = 0 net.ipv4.conf.all.rp_filter = 0 net.ipv4.neigh.default.gc_stale_time = 120 net.ipv4.conf.default.arp_announce = 2 net.ipv4.conf.all.arp_announce = 2 net.ipv4.conf.lo.arp_announce = 2 # TCP Memory net.core.rmem_default = 2097152 net.core.wmem_default = 2097152 net.core.rmem_max = 4194304 net.core.wmem_max = 4194304 net.ipv4.tcp_rmem = 4096 8192 4194304 net.ipv4.tcp_wmem = 4096 8192 4194304 net.ipv4.tcp_mem = 524288 699050 1048576 # TCP SYN net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_synack_retries = 1 net.ipv4.tcp_syn_retries = 1 net.ipv4.tcp_max_syn_backlog = 16384 net.core.netdev_max_backlog = 16384 # TIME_WAIT net.ipv4.route.gc_timeout = 100 net.ipv4.tcp_max_tw_buckets = 5000 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_timestamps = 0 net.ipv4.tcp_fin_timeout = 2 net.ipv4.ip_local_port_range = 20000 50000 # TCP keepalive net.ipv4.tcp_keepalive_probes = 3 net.ipv4.tcp_keepalive_time = 60 net.ipv4.tcp_keepalive_intvl = 10 # Other TCP net.ipv4.tcp_max_orphans = 65535 net.core.somaxconn = 16384 net.ipv4.tcp_sack = 1 net.ipv4.tcp_window_scaling = 1 vm.max_map_count=262144 vm.min_free_kbytes=512000 vm.swappiness = 0""" KERNEL_KEYWORD = [ "net.ipv6.conf.all.disable_ipv6", "net.ipv6.conf.default.disable_ipv6", "net.ipv4.conf.default.rp_filter", "net.ipv4.conf.all.rp_filter", "net.ipv4.neigh.default.gc_stale_time", "net.ipv4.conf.default.arp_announce", "net.ipv4.conf.all.arp_announce", "net.ipv4.conf.lo.arp_announce", "net.core.rmem_default", "net.core.wmem_default", "net.core.rmem_max", "net.core.wmem_max", "net.ipv4.tcp_rmem", "net.ipv4.tcp_wmem", "net.ipv4.tcp_mem", "net.ipv4.tcp_syncookies", "net.ipv4.tcp_synack_retries", "net.ipv4.tcp_syn_retries", "net.ipv4.tcp_max_syn_backlog", "net.core.netdev_max_backlog", "net.ipv4.route.gc_timeout", "net.ipv4.tcp_max_tw_buckets", "net.ipv4.tcp_tw_reuse", "net.ipv4.tcp_timestamps", "net.ipv4.tcp_fin_timeout", "net.ipv4.ip_local_port_range", "net.ipv4.tcp_keepalive_probes", "net.ipv4.tcp_keepalive_time", "net.ipv4.tcp_keepalive_intvl", "net.ipv4.tcp_max_orphans", "net.core.somaxconn", "net.ipv4.tcp_sack", "net.ipv4.tcp_window_scaling", "vm.max_map_count", "vm.swappiness", "vm.min_free_kbytes" ] TO_MODIFY_HOST_NAME = [ "localhost", "localhost.localhost", "localhost.domain", ] class BaseInit(object): """ Base class 检查权限 / 执行命令方法 """ def check_permission(self): """ 检查权限 """ logger.info("开始检查当前用户执行权限") if not os.getuid() == 0: self.__check_is_sodu() if not self.is_sudo: logging.error('当前执行用户不是root,且此用户没有sudo NOPASSWD 权限,无法初始化!') exit(1) logger.info('当前用户权限正常,开始执行脚本') def __check_is_sodu(self): """ 是否具有 sodu 权限 """ logger.info("检查是否具有sodu免密码权限") _cmd = "sudo -n 'whoami' &>/dev/null" _, _, _code = self.cmd(_cmd) self.is_sudo = _code == 0 logger.info("是否具有sodu免密码权限: {}".format(self.is_sudo)) def cmd(self, command): """ 执行shell 命令 """ if hasattr(self, 'is_sudo'): if command.lstrip().startswith("echo"): command = "sudo sh -c '{0}'".format(command) else: command = "sudo {0}".format(command) logger.debug("Exec command: {0}".format(command)) p = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True, ) stdout, stderr = p.communicate() _out, _err, _code = stdout, stderr, p.returncode logger.debug( "Get command({0}) stdout: {1}; stderr: {2}; ret_code: {3}".format( command, _out, _err, _code ) ) return _out, _err, _code @staticmethod def read_file(path, mode='r', res='str'): """ :param path 路径 :param mode 模式 :param res 返回数据类型 str/list """ if not os.path.exists(path): logger.error('读取文件失败,文件路径错误:{}'.format(path)) exit(1) with open(path, mode) as f: data = f.read() if res == 'str' else f.readlines() return data def __get_os_version(self): logging.debug('开始获取系统版本信息') # match = False _, _, _code = self.cmd('systemctl --version') if _code != 0: logger.error("执行失败,当前操作系统不支持本脚本") exit(1) self.os_version = 7 logging.debug('获取系统版本信息完成') def set_opts(self, **kwargs): """ 根据kwargs 设置参数""" raise Exception("程序错误,需实现set_opts方法") def run(self): # 检查权限 if sys.argv[1] != 'valid': self.check_permission() self.__get_os_version() logging.info("开始执行脚本") self.run_methods() def run_methods(self): try: assert isinstance(self.m_list, list), "m_list 类型错误 方法错误,请检查脚本" assert len(self.m_list) > 0, "m_list 为空,请检查脚本" for func_info in self.m_list: assert isinstance(func_info, tuple) and len( func_info) == 2, "todo_list 方法错误,请检查脚本:{}".format(func_info) method_name, method_note = func_info if hasattr(self, method_name): f = getattr(self, method_name) logger.info("开始 执行: {}".format(method_note)) f() logger.info("执行 完成: {}".format(method_note)) else: logger.warn("安装方法列表错误,{} 方法不存在".format(method_note)) else: logging.info("执行结束, 完整日志保存在 {}".format(log_path)) except TypeError: logger.error("脚本配置错误,TypeError:") except Exception as e: logger.error(e) logging.info("执行结束, 完整日志保存在 {}".format(log_path)) exit(1) class InitHost(BaseInit): """ 初始化节点信息 """ def __init__(self, host_name, local_ip): self.m_list = [ ('env_set_timezone', '设置时区'), ('env_set_firewall', '关闭防火墙'), ('env_set_disable_ipv6', '设置关闭ipv6'), ('env_set_language', '设置语言'), ('env_set_file_limit', '设置文件句柄数'), ('env_set_kernel', '设置内核参数'), ('env_set_disable_selinux', '关闭selinux'), ('set_hostname', '设置主机名'), ] # TODO self.hostname = host_name self.local_ip = local_ip def env_set_timezone(self): """ 设置时区 """ timezone = "PRC" self.cmd("test -f /etc/timezone && rm -f /etc/timezone") self.cmd("rm -f /etc/localtimze") self.cmd( "ln -sf /usr/share/zoneinfo/{0} /etc/localtime".format(timezone)) def env_set_firewall(self): """ 关闭 firewall """ _, _, _code = self.cmd( "systemctl status firewalld.service | egrep -q 'Active: .*(dead)'" ) if _code != 0: self.cmd("systemctl stop firewalld.service >/dev/null 2>&1") self.cmd("systemctl disable firewalld.service >/dev/null 2>&1") def env_set_disable_ipv6(self): """ 关闭ipv6 """ _, _, _code = self.cmd("grep -q 'ipv6.disable' /etc/default/grub") if _code == 0: self.cmd( "sed -i 's/ipv6.disable=[0-9]/ipv6.disable=1/g' /etc/default/grub" ) else: self.cmd( """sed -i '/GRUB_CMDLINE_LINUX/ s/="/="ipv6.disable=1 /' /etc/default/grub""" ) self.cmd("sysctl -w net.ipv6.conf.all.disable_ipv6=1") def env_set_language(self): """ 设置语言 """ self.cmd("localectl set-locale LANG=en_US.UTF-8") def env_set_file_limit(self): """ 设置打开的文件句柄数 """ _file_max_out, _, _ = self.cmd("cat /proc/sys/fs/file-max") file_max = int(_file_max_out) _nr_open_out, _, _ = self.cmd("cat /proc/sys/fs/nr_open") nr_open = int(_nr_open_out) if file_max < 655350: self.cmd("sed -i '/fs.file-max/d' /etc/sysctl.conf") self.cmd("echo 'fs.file-max = 655350' >>/etc/sysctl.conf") self.cmd("sysctl -p 1>/dev/null") file_max = 655350 elif file_max > nr_open: file_max = nr_open - 5000 self.cmd("sed -i '/nofile/d' /etc/security/limits.conf") self.cmd( 'echo "* - nofile {0}" >>/etc/security/limits.conf'.format( file_max ) ) if os.path.exists("/etc/security/limits.d/20-nproc.conf"): self.cmd( "sed -i 's#4096#unlimited#g' /etc/security/limits.d/20-nproc.conf" ) self.cmd("sed -i '/^DefaultLimitCORE/d' /etc/systemd/system.conf") self.cmd("sed -i '/^DefaultLimitNOFILE/d' /etc/systemd/system.conf") self.cmd("sed -i '/^DefaultLimitNPROC/d' /etc/systemd/system.conf") c = 'echo -e "DefaultLimitCORE=infinity\\nDefaultLimitNOFILE={0}\\nDefaultLimitNPROC={0}" >>/etc/systemd/system.conf'.format( file_max) self.cmd( c ) self.cmd("ulimit -SHn {0}".format(file_max)) def env_set_kernel(self): """ 设置内核参数 """ for item in KERNEL_KEYWORD: self.cmd('sed -i "/{0}/d" /etc/sysctl.conf'.format(item.strip())) self.cmd('sed -i "/tables/d" /etc/sysctl.conf') self.cmd('echo "{0}" >>/etc/sysctl.conf'.format(KERNEL_PARAM)) self.cmd("sysctl -p 1>/dev/null") def env_set_disable_selinux(self): """ 禁用 selinux """ if os.path.exists("/etc/selinux/config"): self.cmd( "sed -i 's#^SELINUX=.*#SELINUX=disabled#g' /etc/selinux/config") self.cmd("setenforce 0") def set_hostname(self): """设置主机名""" _out, _err, _code = self.cmd("echo $(hostname)") if _out.strip().lower() in TO_MODIFY_HOST_NAME or _out.strip().isdigit(): self.cmd('echo "{0}" >/etc/hostname'.format(self.hostname)) self.cmd('echo "{0}" > /proc/sys/kernel/hostname'.format(self.hostname)) self.cmd("hostname {0}".format(self.hostname)) self.cmd('echo "{0} {1}" >> /etc/hosts'.format(self.local_ip, self.hostname)) class ValidInit(BaseInit): def __init__(self): self.m_list = [ ('valid_env_timezone', '校验时区'), ('valid_env_firewall', '校验防火墙'), ('valid_env_language', '校验语言'), ('valid_env_file_limit', '校验文件具柄数'), ('valid_env_kernel', '校验内核参数'), ('valid_env_disable_selinux', '校验selinux'), ('valid_host_name', '校验host_name'), ] def valid_env_timezone(self): """ 校验时区 """ assert os.readlink( '/etc/localtime') == "/usr/share/zoneinfo/PRC", "时区校验失败" def valid_env_firewall(self): """ 校验防火墙 """ _, _, _code = self.cmd( "systemctl status firewalld.service | egrep -q 'Active: .*(dead)'" ) assert _code == 0, "防火墙校验失败" def valid_env_language(self): """ 校验语言 """ assert self.cmd( "localectl status |grep LANG=en_US.UTF-8")[2] == 0, "语言环境校验失败" def valid_env_file_limit(self): """ 校验文件具柄数 """ _err = "" _file_max_out, _, _ = self.cmd("cat /proc/sys/fs/file-max") file_max = int(_file_max_out) _nr_open_out, _, _ = self.cmd("cat /proc/sys/fs/nr_open") nr_open = int(_nr_open_out) if file_max < 655350: _err = "文件句柄数校验失败" elif file_max > nr_open: file_max = nr_open - 5000 if self.cmd( 'grep "* - nofile {0}" /etc/security/limits.conf'.format( file_max ) )[2] != 0: _err = "文件 /etc/security/limits.conf 校验失败" if os.path.exists("/etc/security/limits.d/20-nproc.conf"): if self.cmd( "grep unlimited /etc/security/limits.d/20-nproc.conf" )[2] != 0: _err = "文件 /etc/security/limits.d/20-nproc.conf 校验失败" if self.cmd('grep "DefaultLimitCORE=infinity" /etc/systemd/system.conf')[2] != 0: _err = "文件 /etc/systemd/system.conf DefaultLimitCORE 校验失败" if self.cmd('grep DefaultLimitNOFILE={0} /etc/systemd/system.conf'.format(file_max))[2] != 0: _err = "文件 /etc/systemd/system.conf DefaultLimitNOFILE 校验失败" if self.cmd('grep DefaultLimitNPROC={0} /etc/systemd/system.conf'.format(file_max))[2] != 0: _err = "文件 /etc/systemd/system.conf DefaultLimitNPROC 校验失败" assert _err == '', _err def valid_env_kernel(self): """ 校验内核参数 """ _list = [i.strip() for i in self.read_file('/etc/sysctl.conf', res='list') if not i.strip().startswith('#')] for i in KERNEL_PARAM.split('\n'): if i.startswith('#'): continue assert i.strip() in _list, "内核参数校验失败: {}".format(i) def valid_env_disable_selinux(self): """ 校验selinux """ assert "SELINUX=disabled" in [ i.strip() for i in self.read_file('/etc/selinux/config', res='list') if not i.strip().startswith('#') ], "selinux 校验失败" def valid_host_name(self): """校验host_name不含localhost""" _out, _err, _code = self.cmd("echo $(hostname)") assert _out.strip().lower() not in TO_MODIFY_HOST_NAME and not _out.strip().isdigit(), "校验主机名失败" def add_hostname_analysis(hostname_str): logger.debug("传入主机信息:\n{}".format(hostname_str)) hostnames = json.loads(hostname_str or '[]') with open("/etc/hosts", "r") as f: hosts = f.read() logger.debug("获取主机解析:\n{}".format(hosts)) hosts_analysis_dict = {} for analysis_str in hosts.split("\n"): if analysis_str.lstrip().startswith("#"): continue analysis_list = list( filter( lambda x: x, analysis_str.strip().replace("\t", " ").split(" ") ) ) if not analysis_list: continue hosts_analysis_dict[analysis_list[0]] = analysis_str for hostname_dict in hostnames: ip = hostname_dict.get("ip") hostname = hostname_dict.get("hostname") host_analysis_str = hosts_analysis_dict.get(ip, "") if not host_analysis_str: hosts += "{} {}\n".format(ip, hostname) elif hostname in host_analysis_str: continue else: host_analysis_str_new = "{} {}".format(host_analysis_str, hostname) hosts = hosts.replace(host_analysis_str, host_analysis_str_new) logger.debug("对比获得最新主机信息:\n{}".format(hosts)) with open("/etc/hosts", "w") as f: f.write(hosts) logger.debug("写入最新主机信息成功!") def usage(error=None): script_full_path = os.path.join(CURRENT_DIR, os.path.basename(__file__)) print("""{0} 脚本 功能为初始化节点,职能包括: 设置时区、关闭防火墙、设置文件具柄和内核参数等 Command: init 初始化节点 valid 校验初始化结果 init_valid 初始化节点,在完成初始化后执行校验 Use "python {0} " for more information about a given command. """.format(script_full_path)) if error is not None: print("Error: {}".format(error)) exit(1) exit(0) def main(): command_list = ('init', 'valid', 'init_valid', 'help', 'write_hostname') try: if sys.argv[1] not in command_list: usage(error='参数错误: {}'.format(sys.argv[1:])) if sys.argv[1] in ['init', 'init_valid']: if len(sys.argv) != 4: usage(error='参数错误: {}'.format(sys.argv[1:])) host_name = sys.argv[2] local_ip = sys.argv[3] init = InitHost(host_name, local_ip) init.run() logger.info("init success") if sys.argv[1] == 'init_valid': check = ValidInit() check.run() logger.info("valid success") elif sys.argv[1] == 'valid': check = ValidInit() check.run() logger.info("valid success") elif sys.argv[1] == "write_hostname": hosts_info = sys.argv[2] # '[{"ip":"10.0.9.18","hostname":"docp-9-18"}]' add_hostname_analysis(hosts_info) else: usage() except Exception as e: usage(error="参数错误, {}".format(e)) if __name__ == '__main__': main() ================================================ FILE: package_hub/_modules/inspection_common.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Jayden Liu # Description: common function for inspection import os import time import json import socket import psutil def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid): if pid and isinstance(pid, int): return "True" else: return "False" def GetProcess_Port(pid): if pid and isinstance(pid, int): try: port = [] cmd = 'ss -lnput | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[4].split(':') port.append(port_aa[-1]) port = list(set(port)) return port except Exception: return None else: return None def GetProcess_Runtime(pid): runtime = None if pid and isinstance(pid, int): try: cmd = 'ps -eo pid,etime|grep ' + str(pid) etime = os.popen(cmd).read().strip('\n').split() # if '-' in etime[1]: # runtime = etime[1].replace('-', ' day ') # else: # runtime = etime[1] runtime = etime[1].replace( '-', '天').replace(':', '小时', 1).replace(':', '分钟', 1) + '秒' run_time = etime[1].split(':') run_time = [int(i) for i in run_time] if len(run_time) == 1: runtime = f"{run_time[0]}秒" elif len(run_time) == 2: runtime = f"{run_time[0]}分钟{run_time[1]}秒" elif len(run_time) == 3: runtime = f"{run_time[0]}小时{run_time[1]}分钟{run_time[2]}秒" elif len(run_time) == 4: runtime = \ f"{run_time[0]}天{run_time[1]}小时{run_time[2]}分钟{run_time[3]}秒" elif len(run_time) == 5: runtime = \ f"{run_time[0]}年{run_time[1]}天{run_time[2]}小时" \ f"{run_time[3]}分钟{run_time[4]}秒" except Exception: pass return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid): if pid and isinstance(pid, int): try: pid_cpuinfo = {} p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] cpu_usage = "{:.2f}".format(cpus_percent) + "%" except Exception: cpu_usage = None else: cpu_usage = None return cpu_usage def GetProcess_Mem(pid): if pid and isinstance(pid, int): try: p = psutil.Process(pid) process_mem = p.memory_percent() mem_usage = "{:.2f}".format(process_mem) + "%" except Exception: return None else: mem_usage = None return mem_usage def GetProcess_ServiceMem(pid, is_java=False): if is_java: if pid and isinstance(pid, int): try: cmd = 'ps -eo pid,command|grep %s' % (pid) process_list = os.popen(cmd).read().strip('\n').split('-Xms') process_mem = process_list[-1].split() service_mem = process_mem[0] return service_mem except Exception: return None else: return None else: return None def GetCluster_IP(json_path="/data/app/data.json", service_name=""): cluster_ip = [] if json_path.endswith("json"): if not os.path.exists(json_path): return [] # raise FileNotFoundError("json file not exist") with open(json_path, "r") as f: content = json.load(f) open_source_service = content.get("basics", []) internl_service = content.get("services", []) open_source_service.extend(internl_service) all_service = open_source_service for service in all_service: if service_name == service.get("name"): service_ip = service.get("local_ip") cluster_ip.append(service_ip) return cluster_ip elif json_path.endswith("list"): try: cmd = 'grep %s %s' % (service_name, json_path) cluster_list = os.popen(cmd).read().strip('\n').split('\n') for cluster_line in cluster_list: cluster = cluster_line.split() cluster_ip.append(cluster[0]) except Exception: return cluster_ip else: return cluster_ip ================================================ FILE: package_hub/_modules/kafka_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get kafka Inspection data import json import os.path as up import os import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_ServiceMem, GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP def GetProcess_Pid(): try: cmd = "ps -eo pid,command |grep 'kafka/bin/..' | grep -v grep" cmd_list = os.popen(cmd).read().strip('\n').split() pid = int(cmd_list[0]) return pid except Exception: return None def GetProcess_LogLevel(pid): log_level = None if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) kafka_path = up.abspath(up.join(p.exe(), "../../..")) f = open('%s/kafka/config/log4j.properties' % (kafka_path), 'r') for lines_list in f: if 'log4j.rootLogger=' in lines_list: kafka_log_level = lines_list.strip('\n').split('=') log_level = kafka_log_level[-1] return log_level except Exception: return None else: return None def GetKafka_PartitionSize(pid, port): if pid and type(pid).__name__ == 'int': try: topic_size_json = {} p = psutil.Process(pid) kafka_path = up.abspath(up.join(p.exe(), "../../..")) cmd = '%s/kafka/bin/kafka-topics.sh --list --bootstrap-server %s:%s' % ( kafka_path, GetLocal_Ip(), port) topic_list = os.popen(cmd).read().strip('\n').split('\n') f = open('%s/kafka/config/server.properties' % (kafka_path), 'r') for lines_list in f: if 'log.dirs=' in lines_list: kafka_data_path = lines_list.strip('\n').split('=') data_path = kafka_data_path[-1] for topic in topic_list: size_cmd = 'du -csh %s/%s' % (data_path, topic) + '-*' topic_size_list = os.popen( size_cmd).read().strip('\n').split('\n') topic_size = topic_size_list[-1].split() topic_size_json[topic] = topic_size[0] except Exception: topic_size_json = None return topic_size_json def GetKafka_PartitionCount(pid, port): if pid and type(pid).__name__ == 'int': partition_size_json = {} try: p = psutil.Process(pid) kafka_path = up.abspath(up.join(p.exe(), "../../..")) cmd = '%s/kafka/bin/kafka-topics.sh --describe --bootstrap-server %s:%s' % ( kafka_path, GetLocal_Ip(), port) topic_list = os.popen(cmd).read().strip('\n').split('\n') for topic_partition in topic_list: if 'PartitionCount' in topic_partition: partition_list = topic_partition.split() topic_line = partition_list[0].split(':') partition_line = partition_list[1].split(':') replication_line = partition_list[2].split(':') topic = topic_line[-1] replication = replication_line[-1] partition = partition_line[-1] partition_size_json[topic] = { "partition": partition, "replication": replication} except Exception: partition_size_json = None return partition_size_json def GetKafka_Offsets(pid, port): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) kafka_path = up.abspath(up.join(p.exe(), "../../..")) cmd = '%s/kafka/bin/kafka-consumer-groups.sh --list --bootstrap-server %s:%s' % ( kafka_path, GetLocal_Ip(), port) group_list = os.popen(cmd).read().strip('\n').split('\n') offset_group_json = {} for group in group_list: log_offset = 0 lag_offset = 0 offset_cmd = '%s/kafka/bin/kafka-consumer-groups.sh --group %s --describe --bootstrap-server %s:%s|sort 2>/dev/null' % ( kafka_path, group, GetLocal_Ip(), port) offset_list = os.popen( offset_cmd).read().strip('\n').split('\n') if "Error" in offset_list[0]: continue offset_json = {} for offsets in offset_list: if 'PARTITION' not in offsets: offset = offsets.split() if offset[0] not in offset_json: if offset[3] != '-': log_offset = int(offset[3]) if offset[4] != '-': lag_offset = int(offset[4]) offset_json[offset[0]] = { "log_offset": log_offset, "lag_offset": lag_offset} else: if offset[3] != '-': log_offset += int(offset[3]) if offset[4] != '-': lag_offset += int(offset[4]) offset_json[offset[0]] = { "log_offset": log_offset, "lag_offset": lag_offset} offset_group_json[group] = offset_json return offset_group_json except Exception: return None def main(pid=GetProcess_Pid(), port='18108', json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid, is_java=True) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["topic_partition"] = GetKafka_PartitionCount(pid, port) process_message["kafka_offsets"] = GetKafka_Offsets(pid, port) process_message["topic_size"] = GetKafka_PartitionSize(pid, port) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="kafka") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/minio_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Jayden Liu # Description: get minio Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'minio': pid = int(p.ppid()) if pid != 1: return pid else: return pnum except Exception: pass except Exception: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = None process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="minio") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/mysql_bak.sh.tmp ================================================ #!/bin/bash # omp自带变量 # 注意备份路径需要单独创建 不可和其他文件共用一个路径 backupDir="${cw_o_data_dir}/backup/mysql" MySQLBin="${cw_o_base_dir}/bin/mysql" MySQLDump="${cw_o_base_dir}/bin/mysqldump" mysqlPort=${cw_o_service_port} mysqlUser="${cw_o_username}" mysqlPasswd="${cw_o_password}" ip="${cw_o_ip}" # 自定义变量 db_name=${db_name} no_pass=${no_pass} need_push=${need_push} # 公共变量 service_name="mysql" BasePath=$(cd `dirname $0`; pwd) nowTime=$(date +%Y%m%d%H%M) count=0 # 请求重试机制 max_count=6 function request_omp() { if [[ -z "$rq_body" ]]; then if [[ -z "$1" ]]; then echo "need request result" exit 1 fi # $1 结果(必填) $2 消息 $3 omp抓取路径 要确定是一个单独的文件 rq_body="{\"result\":\"$1\",\"message\":\"$2\",\"remote_path\":\"$3\",\"ip\":\"$ip\",\"need_push\":\"$need_push\"}" fi RES=$(curl --location --request POST 'http://${cw_o_master_url}p/' --header 'Content-Type: application/json' --data "$rq_body"|grep "\"code\":0") code=$? # 存在返回证明有异常 或者状态码非0 if [[ -z "$RES" ]] || [[ $code != 0 ]];then echo $RES if [[ "$count" -lt "$max_count" ]];then sleep 5 let count+=1 request_omp $1 $2 $3 fi exit 1 else exit 0 fi } # 状态码 备份路径 function touch_zip() { if [[ -z "$2" ]]; then request_omp 1 "need backup path but not provided" fi back_length=$(echo $2 |sed 's#/# #g'|awk '{print NF}') # 保护机制 if [[ "$back_length" -lt 2 ]];then request_omp 1 "backup path is not available" fi tar_name=${service_name}-backup-${nowTime}.tar.gz cd $BasePath && tar -zcf $tar_name -C $2 . --remove-files tar_path=${BasePath}/${tar_name} request_omp $1 "success" $tar_path } function check_mysql(){ if [[ ! -d $backupDir ]]; then mkdir -p $backupDir fi if [ -n "$no_pass" ]; then $MySQLBin -P$mysqlPort -u$mysqlUser -h'127.0.0.1' -e 'exit' >/dev/null 2>&1 databases=$($MySQLBin -u$mysqlUser -h'127.0.0.1' -P$mysqlPort -e 'show databases;' 2>/dev/null | egrep -v 'information_schema|binlogs|mysql|test|Database|performance_schema|hive') else $MySQLBin -P$mysqlPort -u$mysqlUser -p$mysqlPasswd -h'127.0.0.1' -e 'exit' >/dev/null 2>&1 databases=$($MySQLBin -u$mysqlUser -p$mysqlPasswd -h'127.0.0.1' -P$mysqlPort -e 'show databases;' 2>/dev/null | egrep -v 'information_schema|binlogs|mysql|Database|performance_schema|hive') fi if [ $? -ne 0 ]; then request_omp 1 "Error: MySQL User and Password Error." exit 1 fi } function backup_mysql(){ if [ -n "$db_name" ]; then databases=$(echo $db_name |sed 's#,# #g') fi for dataName in $databases; do if [ -n "$no_pass" ]; then $MySQLDump --single-transaction -P$mysqlPort -u$mysqlUser -h'127.0.0.1' -a --default-character-set=utf8 --skip-comments $dataName 2>/dev/null >$backupDir/$dataName-$nowTime.sql else $MySQLDump --single-transaction -P$mysqlPort -u$mysqlUser -p$mysqlPasswd -h'127.0.0.1' -a --default-character-set=utf8 --skip-comments $dataName 2>/dev/null >$backupDir/$dataName-$nowTime.sql fi touch_zip $? $backupDir done } check_mysql backup_mysql ================================================ FILE: package_hub/_modules/mysql_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get mysql Inspection data import json import subprocess import psutil import pymysql from inspection_common import GetProcess_Runtime, GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Mem, \ GetProcessCPU_Pre def run_cmd(cmd): """ 运行系统命令,返回标准输出,标准错误输出及执行状态码 :param cmd: :return: """ p = subprocess.run( cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) cmd_stdout = bytes.decode(p.stdout) if cmd_stdout.endswith('\n'): cmd_stdout = cmd_stdout.strip() # cmd_stderr = bytes.decode(p.stderr) if p.returncode == '0': return None else: # return cmd_stdout, cmd_stderr, p.returncode return cmd_stdout def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'mysqld': pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_Threads(pid): if pid and type(pid).__name__ == 'int': try: # port = [] p = psutil.Process(pid) process_threads = p.num_threads() return process_threads except Exception: return None else: return None def get_connection(host, user, passwd, port, database=None): try: host = GetLocal_Ip() if database: conn = pymysql.connect(host=host, user=user, passwd=passwd, port=port, database=database, charset='utf8', use_unicode=True, cursorclass=pymysql.cursors.DictCursor) else: conn = pymysql.connect(host=host, user=user, passwd=passwd, port=port, charset='utf8', use_unicode=True, cursorclass=pymysql.cursors.DictCursor) return conn except pymysql.Error: return None def GetMysql_ConnNum(user, passwd, port, database): host = GetLocal_Ip() connected_sql = "show status where Variable_name='Threads_connected';" conn_conn = get_connection(host, user, passwd, port, database) if not conn_conn: return None conn_cursor = conn_conn.cursor() try: conn_cursor.execute(connected_sql) conn_conn.commit() exe_result = conn_cursor.fetchone() except conn_conn.Error: exe_result = None finally: conn_cursor.close() conn_conn.close() if exe_result: conn_num = exe_result['Value'] else: conn_num = '' return conn_num def GetMysql_Backup(user, passwd, port, database): host = GetLocal_Ip() check_sql = 'show slave status;' check_conn = get_connection(host, user, passwd, port, database) if not check_conn: return None check_cursor = check_conn.cursor() try: check_cursor.execute(check_sql) check_conn.commit() exe_result = check_cursor.fetchone() except check_conn.Error: exe_result = None return None finally: check_cursor.close() check_conn.close() if exe_result: slave_io_status = exe_result['Slave_IO_Running'] slave_sql_status = exe_result['Slave_SQL_Running'] if slave_io_status == 'Yes' and slave_sql_status == 'Yes': backup_status = 'up' else: backup_status = 'down' else: backup_status = 'no slave' return backup_status def GetAbortedClients_Num(user, passwd, port, database): host = GetLocal_Ip() aborted_clients_num_sql = "show global status like 'Aborted_clients';" aborted_clients_num_conn = get_connection( host, user, passwd, port, database) if not aborted_clients_num_conn: return None aborted_clients_num_cursor = aborted_clients_num_conn.cursor() try: aborted_clients_num_cursor.execute(aborted_clients_num_sql) aborted_clients_num_conn.commit() aborted_clients_num_result = aborted_clients_num_cursor.fetchone() except aborted_clients_num_conn.Error: return None finally: aborted_clients_num_cursor.close() aborted_clients_num_conn.close() aborted_clients_num = aborted_clients_num_result['Value'] return aborted_clients_num def GetFailConnect_Num(user, passwd, port, database): host = GetLocal_Ip() failure_connect_num_sql = "show global status like 'Aborted_connects';" failure_connect_num_conn = get_connection( host, user, passwd, port, database) if not failure_connect_num_conn: return None failure_connect_num_cursor = failure_connect_num_conn.cursor() try: failure_connect_num_cursor.execute(failure_connect_num_sql) failure_connect_num_conn.commit() failure_connect_num_result = failure_connect_num_cursor.fetchone() except failure_connect_num_conn.Error: return None finally: failure_connect_num_cursor.close() failure_connect_num_conn.close() failure_connect_num = failure_connect_num_result['Value'] return failure_connect_num def GetSlowQuery_Num(user, passwd, port, database): host = GetLocal_Ip() select_slow_query_switch_sql = "show variables where variable_name='slow_query_log';" select_slow_query_log_file_sql = "show variables where variable_name='slow_query_log_file';" select_slow_query_log_file = '' slow_query_num = '' select_slow_query_num_conn = get_connection( host, user, passwd, port, database) if not select_slow_query_num_conn: return None select_slow_query_num_cursor = select_slow_query_num_conn.cursor() try: select_slow_query_num_cursor.execute(select_slow_query_switch_sql) select_slow_query_num_conn.commit() switch_exe_result = select_slow_query_num_cursor.fetchone() except select_slow_query_num_conn.Error: return None if not switch_exe_result: return None if switch_exe_result['Value'] != 'ON': return None try: select_slow_query_num_cursor.execute(select_slow_query_log_file_sql) select_slow_query_num_conn.commit() file_exe_result = select_slow_query_num_cursor.fetchone() except select_slow_query_num_conn.Error: return None finally: select_slow_query_num_cursor.close() select_slow_query_num_conn.close() if not file_exe_result: return None select_slow_query_log_file = file_exe_result['Value'] get_slow_query_num_cmd = "grep Query_time {} | wc -l".format( select_slow_query_log_file) grep_wc_result = run_cmd(get_slow_query_num_cmd) if not grep_wc_result: return None slow_query_num = grep_wc_result return slow_query_num def main(pid=GetProcess_Pid(), host='127.0.0.1', user='Rootmaster', passwd='Rootmaster@777', port=18103, database=None, **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message['max_memory'] = None process_message["log_level"] = None process_message["process_threads"] = GetProcess_Threads(pid) process_message["conn_num"] = GetMysql_ConnNum( user, passwd, port, database) process_message["aborted_clients"] = GetAbortedClients_Num( user, passwd, port, database) process_message["failure_connect"] = GetFailConnect_Num( user, passwd, port, database) process_message["slow_query"] = GetSlowQuery_Num( user, passwd, port, database) process_message['backup_status'] = GetMysql_Backup( user, passwd, port, database) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/nacos_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get nacos Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP, GetProcess_ServiceMem def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'java' and 'nacos' in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) nacos_path = p.cwd() f = open('%s/conf/nacos-logback.xml' % (nacos_path), 'r') context = f.readlines() nacos_log_level = context[-5].strip('\n').split('"') log_level = nacos_log_level[1] except Exception: log_level = None return log_level else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid, is_java=True) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="nacos") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/ntpd_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Jayden Liu # Description: get apmConsumer Inspection data import json import os from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetProcess_ServiceMem def GetProcess_Pid(): try: cmd = "ps -eo pid,command |grep 'ntpd' | grep -v grep" cmd_list = os.popen(cmd).read().strip('\n').split() pid = int(cmd_list[0]) return pid except Exception: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = None process_message["cluster_ip"] = None return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/postgreSql_bak.sh.tmp ================================================ #!/bin/bash # omp自带变量 # 注意备份路径需要单独创建 不可和其他文件共用一个路径 PgSqlPort=${cw_o_service_port} PgSqlUser="${cw_o_username}" PgSqlPasswd="${cw_o_password}" ip="${cw_o_ip}" PgSqlBaseDir="${cw_o_base_dir}" pgSqlDataDir="${cw_o_data_dir}" backupDir="./backup/pgsql" pgSqlScripts="${PgSqlBaseDir}/scripts/postgreSql" PgSqlDump="${PgSqlBaseDir}/bin/pg_dump" PgSqlDumpAll="${PgSqlBaseDir}/bin/pg_dumpall" # 自定义变量 db_name=${db_name} need_push=${need_push} need_app=${need_app} # 公共变量 service_name="postgreSql" BasePath=$(cd `dirname $0`; pwd) nowTime=$(date +%Y%m%d%H%M) count=0 # 请求重试机制 max_count=6 function request_omp() { if [[ -z "$rq_body" ]]; then if [[ -z "$1" ]]; then echo "need request result" exit 1 fi # $1 结果(必填) $2 消息 $3 omp抓取路径 要确定是一个单独的文件 rq_body="{\"result\":\"$1\",\"message\":\"$2\",\"remote_path\":\"$3\",\"ip\":\"$ip\",\"need_push\":\"$need_push\"}" fi RES=$(curl --location --request POST 'http://${cw_o_master_url}p/' --header 'Content-Type: application/json' --data "$rq_body"|grep "\"code\":0") code=$? # 存在返回证明有异常 或者状态码非0 if [[ -z "$RES" ]] || [[ $code != 0 ]];then echo $RES if [[ "$count" -lt "$max_count" ]];then sleep 5 let count+=1 request_omp $1 $2 $3 fi exit 1 else exit 0 fi } # 状态码 备份路径 function touch_zip() { if [[ -z "$2" ]]; then request_omp 1 "need backup path but not provided" fi back_length=$(echo $2 |sed 's#/# #g'|awk '{print NF}') # 保护机制 if [[ "$back_length" -lt 2 ]];then request_omp 1 "backup path is not available" fi tar_name=${service_name}-backup-${nowTime}.tar.gz cd $BasePath && tar -zcf $tar_name -C $2 . --remove-files tar_path=${BasePath}/${tar_name} request_omp $1 "success" $tar_path } function backup_pgsql() { # 创建备份路径 if [[ ! -d $backupDir ]]; then mkdir -p $backupDir fi #全库备份还是单库备份 if [ -n "$db_name" ]; then databases=$(echo $db_name |sed 's#,# #g') for dataName in $databases; do $PgSqlDump -U $PgSqlUserr -h$PgSqlPort -p18126 $dataName > $backupDir/$dataName.sql done else $PgSqlDumpAll -U $PgSqlUser -h127.0.0.1 -p$PgSqlPort > $backupDir/pg_all_$ip.sql fi dump_code=$? cp_code=0 #是否升级备份 if [ -n "$need_app" ]; then bash $pgSqlScripts stop cp -af $PgSqlBaseDir $backupDir cp -af $pgSqlDataDir $backupDir cp_code=$? bash $pgSqlScripts start fi if [[ $cp_code != 0 ]] || [[ $dump_code != 0 ]];then touch_zip 1 $backupDir else touch_zip 0 $backupDir fi } backup_pgsql ================================================ FILE: package_hub/_modules/postgresql_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get postgresql Inspection data import json import os import os.path as up import socket import time import psutil def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'postmaster' and '-D' in p.cmdline(): pid = pnum return pid except Exception: pass except Exception: return None def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid): if pid and type(pid).__name__ == 'int': return "True" else: return "False" def GetProcess_Port(pid): try: if pid and type(pid).__name__ == 'int': port = [] # p = psutil.Process(pid) cmd = 'ss -tnlp | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[3].split(':') port.append(port_aa[-1]) port = list(set(port)) return port else: return None except Exception: return None def GetProcess_Runtime(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,etime|grep ' + str(pid) etime = os.popen(cmd).read().strip('\n').split() if '-' in etime[1]: runtime = etime[1].replace('-', ' day ') else: runtime = etime[1] except Exception: runtime = None else: runtime = None return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid): if pid and type(pid).__name__ == 'int': try: pid_cpuinfo = {} p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] cpu_usage = "{:.2f}".format(cpus_percent) + "%" except Exception: cpu_usage = None else: cpu_usage = None return cpu_usage def GetProcess_Mem(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) process_mem = p.memory_percent() mem_usage = "{:.2f}".format(process_mem) + "%" except Exception: return None else: mem_usage = None return mem_usage def GetProcess_ServiceMem(pid): if pid and type(pid).__name__ == 'int': try: cmd = 'ps -eo pid,command|grep %s' % (pid) process_list = os.popen(cmd).read().strip('\n').split('-Xms') process_mem = process_list[-1].split() service_mem = process_mem[0] return service_mem except Exception: return None else: return None def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) postgresql_path = up.abspath(up.join(p.exe(), "../..")) f = open('%s/postgresql.conf' % (postgresql_path), 'r') for lines_list in f: if 'log_destination =' in lines_list: postgresql_log_level = lines_list.strip('\n').split() if isinstance(postgresql_log_level[2], str): log_level = postgresql_log_level[2].replace("'", "") else: log_level = postgresql_log_level[2] except Exception: log_level = None return log_level else: return None def GetCluster_IP(pid): if pid and type(pid).__name__ == 'int': try: cluster_ip = [] p = psutil.Process(pid) postgresql_path = up.abspath(up.join(p.exe(), "../../..")) cmd = 'grep postgreSql %s/task.list' % (postgresql_path) cluster_list = os.popen(cmd).read().strip('\n').split('\n') if int(len(cluster_list)) > 1: for cluster_line in cluster_list: cluster = cluster_line.split() cluster_ip.append(cluster[0]) else: cluster_ip = None return cluster_ip except Exception: return None else: return None def main(pid=GetProcess_Pid(), **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP(pid) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/prometheus_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get prometheus Inspection data import json import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'prometheus': pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) if 'log.level=info' in p.cmdline(): log_level = 'info' elif 'log.level=debug' in p.cmdline(): log_level = 'debug' elif 'log.level=warn' in p.cmdline() or 'log.level=error' in p.cmdline(): log_level = 'error' else: log_level = 'info' return log_level except Exception: return None else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="prometheus") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/rocketmq_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get rocketmq Inspection data import os import re import psutil import time import json import socket def GetProcess_Pid(): try: pid_list = [] for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == "java": for cmd_str in p.cmdline(): if "rocketmq.broker" in cmd_str: pid_list.append(pnum) if "rocketmq.namesrv" in cmd_str: pid_list.append(pnum) except Exception: pass return pid_list except Exception: return [] def GetLocal_Ip(): try: csock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) csock.connect(('8.8.8.8', 80)) (addr, port) = csock.getsockname() csock.close() return addr except socket.error: return "127.0.0.1" def GetProcess_Survive(pid_list): if pid_list and isinstance(pid_list, list): return "True" else: return "False" def GetProcess_Port(pid_list): if not pid_list or not isinstance(pid_list, list): return None port = [] for pid in pid_list: try: cmd = 'ss -tnlp | grep ' + str(pid) port_list = os.popen(cmd).read().strip('\n').split('\n') for line_list in port_list: if not line_list: continue line = line_list.split() port_aa = line[3].split(':') port.append(port_aa[-1]) except Exception: return port return list(set(port)) def GetProcess_Runtime(pid_list): runtime = None if not pid_list or not isinstance(pid_list, list): return runtime try: cmd = 'ps -eo pid,etime|grep ' + str(pid_list[0]) etime = os.popen(cmd).read().strip('\n').split() # if '-' in etime[1]: # runtime = etime[1].replace('-', ' day ') # else: # runtime = etime[1] runtime = etime[1].replace( '-', '天').replace(':', '小时', 1).replace(':', '分钟', 1) + '秒' run_time = etime[1].split(':') run_time = [int(i) for i in run_time] if len(run_time) == 1: runtime = f"{run_time[0]}秒" elif len(run_time) == 2: runtime = f"{run_time[0]}分钟{run_time[1]}秒" elif len(run_time) == 3: runtime = f"{run_time[0]}小时{run_time[1]}分钟{run_time[2]}秒" elif len(run_time) == 4: runtime = \ f"{run_time[0]}天{run_time[1]}小时{run_time[2]}分钟{run_time[3]}秒" elif len(run_time) == 5: runtime = \ f"{run_time[0]}年{run_time[1]}天{run_time[2]}小时" \ f"{run_time[3]}分钟{run_time[4]}秒" except Exception: pass return runtime _timer = getattr(time, 'monotonic', time.time) num_cpus = psutil.cpu_count() or 1 def timer(): return _timer() * num_cpus def GetProcessCPU_Pre(pid_list): if not pid_list or not isinstance(pid_list, list): return None cpus_sum = 0 pid_cpuinfo = {} for pid in pid_list: try: p = psutil.Process(pid) pt = p.cpu_times() st1, pt1_0, pt1_1 = timer(), pt.user, pt.system # new st0, pt0_0, pt0_1 = pid_cpuinfo.get(pid, (0, 0, 0)) # old delta_proc = (pt1_0 - pt0_0) + (pt1_1 - pt0_1) delta_time = st1 - st0 cpus_percent = ((delta_proc / delta_time) * 100) cpus_sum += cpus_percent pid_cpuinfo[pid] = [st1, pt1_0, pt1_1] except Exception: pass cpu_usage = "{:.2f}".format(cpus_sum) + "%" return cpu_usage def GetProcess_Mem(pid_list): if not pid_list or not isinstance(pid_list, list): return None process_mem_sum = 0 for pid in pid_list: try: p = psutil.Process(pid) process_mem = p.memory_percent() process_mem_sum += process_mem except Exception: pass mem_usage = "{:.2f}".format(process_mem_sum) + "%" return mem_usage def GetProcess_ServiceMem(pid_list): if not pid_list or not isinstance(pid_list, list): return None service_mem_sum = 0 for pid in pid_list: try: cmd = 'ps -eo pid,command|grep %s' % (pid) process_list = os.popen(cmd).read().strip('\n').split('-ms') process_mem = process_list[-1].split() service_mem = process_mem[0] service_mem_sum += service_mem except Exception: pass return service_mem_sum if service_mem_sum else None def GetProcess_LogLevel(pid_list): if not pid_list or not isinstance(pid_list, list): return None log_level_set = set() pid = pid_list[0] try: p = psutil.Process(pid) path_str = re.compile(r"(.*/rocketmq/conf)/.*") project_path = "" for cmd_str in p.cmdline(): if path_str.findall(cmd_str): project_path = path_str.findall(cmd_str)[0] if not project_path: return "" for log_file in ['logback_broker.xml', 'logback_namesrv.xml', 'logback_tools.xml']: f = open(f'{project_path}/{log_file}', 'r') for lines_list in f: if '', '') log_level_set.add(log_level) break f.close() except Exception: pass return ",".join(log_level_set) def main(pid_list=GetProcess_Pid(), **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid_list) process_message["port_status"] = GetProcess_Port(pid_list) process_message['run_time'] = GetProcess_Runtime(pid_list) process_message['max_memory'] = GetProcess_ServiceMem(pid_list) process_message["mem_usage"] = GetProcess_Mem(pid_list) process_message["cpu_usage"] = GetProcessCPU_Pre(pid_list) process_message["log_level"] = GetProcess_LogLevel(pid_list) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/tengine_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get tengine Inspection data import json import os import os.path as up import psutil from inspection_common import GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Runtime, \ GetProcess_Mem, GetProcessCPU_Pre, GetCluster_IP def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'nginx' and 'master' in p.cmdline(): pid = pnum return pid except Exception: pass except Exception: return None def GetProcess_LogLevel(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) nginx_path = up.abspath(up.join(p.exe(), "../..")) cmd = 'grep access_log %s/conf/vhost/*.conf |wc -l' % (nginx_path) log_list = os.popen(cmd).read().strip('\n') if int(log_list) == 0: log_level = 'error' else: log_level = 'access' return log_level except Exception: return None else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = None process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["cluster_ip"] = GetCluster_IP( json_path=json_path, service_name="tengine") return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/tomcat_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get mysql Inspection data import json import os import psutil from inspection_common import GetProcess_Runtime, GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Mem, \ GetProcessCPU_Pre def GetProcess_Pid(process_name='tomcat'): cmd = "ps aux|grep {}|grep -v grep".format(process_name) try: process_info = os.popen(cmd).read().split() return int(process_info[1]) except Exception: return None def GetProcess_Threads(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) process_threads = p.num_threads() return process_threads except Exception: return None else: return None def main(): pid = GetProcess_Pid() if pid is None: return json.dumps({}) process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message['max_memory'] = None process_message["log_level"] = None process_message["process_threads"] = GetProcess_Threads(pid) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/_modules/zookeeper_check.py ================================================ #!/usr/bin/env python3 # encoding: utf-8 # Author: Darren Liu # Description: get zookeeper Inspection data import json import os import psutil from inspection_common import GetProcess_Runtime, GetLocal_Ip, GetProcess_Survive, GetProcess_Port, GetProcess_Mem, \ GetProcessCPU_Pre, GetProcess_ServiceMem def GetProcess_Pid(): try: for pnum in psutil.pids(): try: p = psutil.Process(pnum) if p.name() == 'java' and 'zookeeper' in p.cwd(): pid = pnum return pid except Exception: pass except Exception: return None def GetNode_Status(pid): if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) cmd = "bash %s/bin/zkServer.sh status 2>/dev/null" % (p.cwd()) node_status = os.popen(cmd).read().strip('\n').split(':') status = node_status[-1].strip() return status except Exception: return None def GetCluster_IP(pid): zk_cluster = [] if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) f = open('%s/conf/zoo.cfg' % (p.cwd()), 'r') for lines_list in f: if 'server' in lines_list: zk_cluster_list = lines_list.strip('\n').split('=') zk_cluster_ip = zk_cluster_list[-1].split(':') zk_cluster.append(zk_cluster_ip[0]) return zk_cluster except Exception: return None else: return None def GetProcess_LogLevel(pid): # zk_cluster = [] log_level = None if pid and type(pid).__name__ == 'int': try: p = psutil.Process(pid) f = open('%s/conf/log4j.properties' % (p.cwd()), 'r') for lines_list in f: if 'zookeeper.root.logger=' in lines_list: zk_log_level = lines_list.strip('\n').split('=') log_level = zk_log_level[-1] return log_level except Exception: return None else: return None def main(pid=GetProcess_Pid(), json_path="/data/app/data.json", **kwargs): process_message = dict() process_message["IP"] = GetLocal_Ip() process_message["service_status"] = GetProcess_Survive(pid) process_message["port_status"] = GetProcess_Port(pid) process_message['run_time'] = GetProcess_Runtime(pid) process_message['max_memory'] = GetProcess_ServiceMem(pid, is_java=True) process_message["mem_usage"] = GetProcess_Mem(pid) process_message["cpu_usage"] = GetProcessCPU_Pre(pid) process_message["log_level"] = GetProcess_LogLevel(pid) process_message["node_status"] = GetNode_Status(pid) process_message["cluster_ip"] = GetCluster_IP(pid) return json.dumps(process_message) if __name__ == '__main__': print(main()) ================================================ FILE: package_hub/back_end_verified/.gitkeep ================================================ ================================================ FILE: package_hub/custom_scripts/.gitkeep ================================================ ================================================ FILE: package_hub/custom_scripts/template.py ================================================ # 自定义脚本模板文件 # 1. 脚本名称须使用蛇形命名法,且以custom_开头 # 2. 类名须为 CustomMetrics # 3. 函数需以get开头,使用蛇形命名法,须为静态方法,无形参,形如 get_xxx() # 4. 函数返回数据格式须为json格式,必须包含 help,type,metric,value 键值对,labels键值对可选 # 5. 不可以引用第三方模块 # 以下为参考内容 class CustomMetrics: @staticmethod def get_node_cpu_guest_seconds_total(): """ 获取guest占用cpu时间 :return: """ return { "help": "node_cpu_guest_seconds_total Seconds the cpus spent in guests (VMs) for each mode.", "type": "counter", "metric": "node_cpu_guest_seconds_total", "value": 4, "labels": { "cpu": "0", "mode": "nice" } } @staticmethod def get_node_runtime(): """ 获取系统运行时间 :return: """ return { "help": "node runtime.", "type": "gauge", "metric": "node_runtime", "value": 2000, "labels": [] } ================================================ FILE: package_hub/data_files/.gitkeep ================================================ ================================================ FILE: package_hub/front_end_verified/.gitkeep ================================================ ================================================ FILE: package_hub/grafana_dashboard_json/ 21-rediscluster-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 21, "uid": "QuS4Sq0Mz1", "title": "RedisCluster 信息面板", "panels": [ { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "s", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 2, "x": 0, "y": 0 }, "id": 9, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "expr": "max(max_over_time(redis_uptime_in_seconds{cluster=\"$cluster\"}[$__interval]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "metric": "", "refId": "A", "step": 1800 } ], "thresholds": "", "title": "Uptime", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 6, "x": 2, "y": 0 }, "hideTimeOverride": true, "id": 12, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "expr": "redis_connected_clients{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ instance_name }}", "metric": "", "refId": "A", "step": 2 } ], "thresholds": "", "timeFrom": "1m", "timeShift": null, "title": "Clients", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 0 }, "hiddenSeries": false, "id": 2, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(redis_commands_processed_total{cluster=~\"$cluster\"}[1m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ instance_name }}", "metric": "A", "refId": "A", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Commands Executed / sec", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 0 }, "hiddenSeries": false, "id": 1, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": true, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(redis_keyspace_hits_total{cluster=~\"$cluster\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "hits {{ instance_name }}", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "irate(redis_keyspace_misses_total{cluster=~\"$cluster\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "misses {{ instance_name }}", "metric": "", "refId": "B", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Hits / Misses per Sec", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "max": "#BF1B00" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "hiddenSeries": false, "id": 7, "isNew": true, "legend": { "avg": false, "current": false, "hideEmpty": false, "hideZero": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "redis_memory_used_bytes{cluster=~\"$cluster\"} ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "used {{ instance_name }}", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "redis_memory_max_bytes{cluster=~\"$cluster\"} ", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "max {{ instance_name }}", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Total Memory Usage", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 10, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(redis_net_input_bytes_total{cluster=~\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "input {{ instance_name }}", "refId": "A", "step": 240 }, { "expr": "rate(redis_net_output_bytes_total{cluster=~\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "output {{ instance_name }}", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Network I/O", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 7, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 5, "isNew": true, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum (redis_db_keys{cluster=~\"$cluster\"}) by (db)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ db }} {{ instance_name }}", "refId": "A", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Total Items per DB", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "none", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 7, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 13, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum (redis_db_keys{cluster=~\"$cluster\"}) - sum (redis_db_keys_expiring{cluster=~\"$cluster\"}) ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "not expiring {{ instance_name }}", "refId": "A", "step": 240, "target": "" }, { "expr": "sum (redis_db_keys_expiring{cluster=~\"$cluster\"}) ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "expiring {{ instance_name }}", "metric": "", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Expiring vs Not-Expiring Keys", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "evicts": "#890F02", "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02", "reclaims": "#3F6833" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 8, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "reclaims", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(redis_expired_keys_total{cluster=~\"$cluster\"}[5m])) by (instance)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "expired {{ instance_name }}", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "sum(rate(redis_evicted_keys_total{cluster=~\"$cluster\"}[5m])) by (instance)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "evicted {{ instance_name }}", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Expired / Evicted", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 8, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 21 }, "hiddenSeries": false, "id": 14, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "topk(5, irate(redis_commands_total{cluster=~\"$cluster\"} [1m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ cmd }} {{ instance_name }}", "metric": "redis_command_calls_total", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Command Calls / sec", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(redis_uptime_in_seconds, cluster)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "集群", "multi": false, "name": "cluster", "options": [], "query": { "query": "label_values(redis_uptime_in_seconds, cluster)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/1-zhu-ji-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 1, "uid": "9CWBz0bik", "title": "主机信息面板", "panels": [ { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "decimals": 0, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 1 }, { "color": "rgba(50, 172, 45, 0.97)", "value": 3 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 0, "y": 0 }, "hideTimeOverride": true, "id": 15, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "avg(time() - node_boot_time_seconds{env=\"$env\",instance=~\"$node\"})", "format": "time_series", "hide": false, "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 40 } ], "title": "运行时间", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "decimals": 1, "mappings": [ { "options": { "0": { "text": "N/A" } }, "type": "value" } ], "max": 100, "min": 0.1, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "#EAB839", "value": 70 }, { "color": "red", "value": 90 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 6, "w": 3, "x": 2, "y": 0 }, "id": 177, "options": { "displayMode": "lcd", "orientation": "horizontal", "reduceOptions": { "calcs": [ "last" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "100 - (avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"idle\"}[$interval])) * 100)", "instant": true, "interval": "", "legendFormat": "总CPU使用率", "refId": "A" }, { "expr": "avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"iowait\"}[$interval])) * 100", "hide": true, "instant": true, "interval": "", "legendFormat": "IOwait使用率", "refId": "C" }, { "expr": "(1 - (node_memory_MemAvailable_bytes{env=\"$env\",instance=~\"$node\"} / (node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"})))* 100", "instant": true, "interval": "", "legendFormat": "内存使用率", "refId": "B" }, { "expr": "(node_filesystem_size_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint=\"$maxmount\"}-node_filesystem_free_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint=\"$maxmount\"})*100 /(node_filesystem_avail_bytes {env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint=\"$maxmount\"}+(node_filesystem_size_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint=\"$maxmount\"}-node_filesystem_free_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint=\"$maxmount\"}))", "hide": false, "instant": true, "interval": "", "legendFormat": "最大分区({{mountpoint}})使用率", "refId": "D" }, { "expr": "(1 - ((node_memory_SwapFree_bytes{env=\"$env\",instance=~\"$node\"} + 1)/ (node_memory_SwapTotal_bytes{env=\"$env\",instance=~\"$node\"} + 1))) * 100", "instant": true, "interval": "", "legendFormat": "交换分区使用率", "refId": "F" } ], "timeFrom": null, "timeShift": null, "transformations": [], "type": "bargauge" }, { "columns": [], "datasource": "Prometheus", "description": "本看板中的:磁盘总量、使用量、可用量、使用率保持和df命令的Size、Used、Avail、Use% 列的值一致,并且Use%的值会四舍五入保留一位小数,会更加准确。\n\n注:df中Use%算法为:(size - free) * 100 / (avail + (size - free)),结果是整除则为该值,非整除则为该值+1,结果的单位是%。\n参考df命令源码:", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fontSize": "80%", "gridPos": { "h": 6, "w": 10, "x": 5, "y": 0 }, "id": 181, "links": [ { "targetBlank": true, "title": "https://github.com/coreutils/coreutils/blob/master/src/df.c", "url": "https://github.com/coreutils/coreutils/blob/master/src/df.c" } ], "pageSize": null, "scroll": true, "showHeader": true, "sort": { "col": 6, "desc": false }, "styles": [ { "alias": "分区", "align": "auto", "colorMode": null, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "mountpoint", "thresholds": [ "" ], "type": "string", "unit": "bytes" }, { "alias": "可用空间", "align": "auto", "colorMode": "value", "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 1, "mappingType": 1, "pattern": "Value #A", "thresholds": [ "10000000000", "20000000000" ], "type": "number", "unit": "bytes" }, { "alias": "使用率", "align": "auto", "colorMode": "cell", "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 1, "mappingType": 1, "pattern": "Value #B", "thresholds": [ "70", "85" ], "type": "number", "unit": "percent" }, { "alias": "总空间", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 0, "link": false, "mappingType": 1, "pattern": "Value #C", "thresholds": [], "type": "number", "unit": "bytes" }, { "alias": "文件系统", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "link": false, "mappingType": 1, "pattern": "fstype", "thresholds": [], "type": "string", "unit": "short" }, { "alias": "设备名", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "link": false, "mappingType": 1, "pattern": "device", "preserveFormat": false, "sanitize": false, "thresholds": [], "type": "string", "unit": "short" }, { "alias": "", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "decimals": 2, "pattern": "/.*/", "preserveFormat": true, "sanitize": false, "thresholds": [], "type": "hidden", "unit": "short" } ], "targets": [ { "expr": "node_filesystem_size_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-0", "format": "table", "hide": false, "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "总量", "refId": "C" }, { "expr": "node_filesystem_avail_bytes {env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-0", "format": "table", "hide": false, "instant": true, "interval": "10s", "intervalFactor": 1, "legendFormat": "", "refId": "A" }, { "expr": "(node_filesystem_size_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}) *100/(node_filesystem_avail_bytes {env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}+(node_filesystem_size_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{env=\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}))", "format": "table", "hide": false, "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "B" } ], "title": "【$node】:各分区可用空间(EXT.*/XFS)", "transform": "table", "type": "table-old" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "decimals": 2, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "max": 100, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(50, 172, 45, 0.97)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 20 }, { "color": "#d44a3a", "value": 50 } ] }, "unit": "percent" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 15, "y": 0 }, "id": 20, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "last" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "avg(rate(node_cpu_seconds_total{instance=~\"$node\",mode=\"iowait\"}[$interval])) * 100", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "timeFrom": null, "timeShift": null, "title": "CPU iowait", "type": "stat" }, { "aliasColors": { "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_cni0_in": "light-red", "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_cni0_in下载": "green", "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_cni0_out上传": "yellow", "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_eth0_in下载": "purple", "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_eth0_out": "purple", "cn-shenzhen.i-wz9cq1dcb6zwc39ehw59_eth0_out上传": "blue" }, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 6, "w": 7, "x": 17, "y": 0 }, "hiddenSeries": false, "id": 183, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "show": false, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 1, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [ { "alias": "/.*_out上传$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "increase(node_network_receive_bytes_total{env=\"$env\",instance=~\"$node\",device=~\"$device\"}[60m])", "interval": "60m", "intervalFactor": 1, "legendFormat": "{{device}}_in下载", "metric": "", "refId": "A", "step": 600, "target": "" }, { "expr": "increase(node_network_transmit_bytes_total{env=\"$env\",instance=~\"$node\",device=~\"$device\"}[60m])", "hide": false, "interval": "60m", "intervalFactor": 1, "legendFormat": "{{device}}_out上传", "refId": "B", "step": 600 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "每小时流量$device", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": "上传(-)/下载(+)", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 1 }, { "color": "rgba(50, 172, 45, 0.97)", "value": 2 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 0, "y": 2 }, "id": 14, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "count(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\", mode='system'})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "CPU 核数", "type": "stat" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 100000 }, { "color": "rgba(50, 172, 45, 0.97)", "value": 1000000 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 15, "y": 2 }, "id": 179, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "avg(node_filesystem_files_free{instance=~\"$node\",mountpoint=\"$maxmount\",fstype=~\"ext.?|xfs\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "剩余节点数:$maxmount ", "type": "stat" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "decimals": 0, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 2 }, { "color": "rgba(50, 172, 45, 0.97)", "value": 3 } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 0, "y": 4 }, "id": 75, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "sum(node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A", "step": 20 } ], "title": "总内存", "type": "stat" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "mappings": [ { "options": { "match": "null", "result": { "text": "N/A" } }, "type": "special" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "rgba(245, 54, 54, 0.9)", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 1024 }, { "color": "rgba(50, 172, 45, 0.97)", "value": 10000 } ] }, "unit": "locale" }, "overrides": [] }, "gridPos": { "h": 2, "w": 2, "x": 15, "y": 4 }, "id": 178, "interval": null, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "avg(node_filefd_maximum{instance=~\"$node\"})", "format": "time_series", "instant": true, "intervalFactor": 1, "legendFormat": "", "refId": "A", "step": 20 } ], "title": "总文件描述符", "type": "stat" }, { "aliasColors": { "192.168.200.241:9100_Total": "dark-red", "Idle - Waiting for something to happen": "#052B51", "guest": "#9AC48A", "idle": "#052B51", "iowait": "#EAB839", "irq": "#BF1B00", "nice": "#C15C17", "sdb_每秒I/O操作%": "#d683ce", "softirq": "#E24D42", "steal": "#FCE2DE", "system": "#508642", "user": "#5195CE", "磁盘花费在I/O操作占比": "#ba43a9" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 6 }, "hiddenSeries": false, "id": 7, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "maxPerRow": 6, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [ { "alias": "/.*总使用率/", "color": "#C4162A", "fill": 0 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"system\"}[$interval])) by (instance) *100", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "系统使用率", "refId": "A", "step": 20 }, { "expr": "avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"user\"}[$interval])) by (instance) *100", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "用户使用率", "refId": "B", "step": 240 }, { "expr": "avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"iowait\"}[$interval])) by (instance) *100", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "磁盘IO使用率", "refId": "D", "step": 240 }, { "expr": "(1 - avg(rate(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\",mode=\"idle\"}[$interval])) by (instance))*100", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "总使用率", "refId": "F", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CPU使用率", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "percent", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "192.168.200.241:9100_总内存": "dark-red", "使用率": "yellow", "内存_Avaliable": "#6ED0E0", "内存_Cached": "#EF843C", "内存_Free": "#629E51", "内存_Total": "#6d1f62", "内存_Used": "#eab839", "可用": "#9ac48a", "总内存": "#bf1b00" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 6 }, "height": "300", "hiddenSeries": false, "id": 156, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "总内存", "color": "#C4162A", "fill": 0 }, { "alias": "使用率", "color": "rgb(0, 209, 255)", "lines": false, "pointradius": 1, "points": true, "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "总内存", "refId": "A", "step": 4 }, { "expr": "node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"} - node_memory_MemAvailable_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "已用", "refId": "B", "step": 4 }, { "expr": "node_memory_MemAvailable_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "可用", "refId": "F", "step": 4 }, { "expr": "node_memory_Buffers_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "内存_Buffers", "refId": "D", "step": 4 }, { "expr": "node_memory_MemFree_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "内存_Free", "refId": "C", "step": 4 }, { "expr": "node_memory_Cached_bytes{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "内存_Cached", "refId": "E", "step": 4 }, { "expr": "node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"} - (node_memory_Cached_bytes{env=\"$env\",instance=~\"$node\"} + node_memory_Buffers_bytes{env=\"$env\",instance=~\"$node\"} + node_memory_MemFree_bytes{env=\"$env\",instance=~\"$node\"})", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "G" }, { "expr": "(1 - (node_memory_MemAvailable_bytes{env=\"$env\",instance=~\"$node\"} / (node_memory_MemTotal_bytes{env=\"$env\",instance=~\"$node\"})))* 100", "format": "time_series", "hide": false, "interval": "30m", "intervalFactor": 10, "legendFormat": "使用率", "refId": "H" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "内存信息", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "percent", "label": "内存使用率", "logBase": 1, "max": "100", "min": "0", "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "192.168.10.227:9100_em1_in下载": "super-light-green", "192.168.10.227:9100_em1_out上传": "dark-blue" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 6 }, "height": "300", "hiddenSeries": false, "id": 157, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_out上传$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_network_receive_bytes_total{env=\"$env\",instance=~'$node',device=~\"$device\"}[$interval])*8", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_in下载", "refId": "A", "step": 4 }, { "expr": "rate(node_network_transmit_bytes_total{env=\"$env\",instance=~'$node',device=~\"$device\"}[$interval])*8", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_out上传", "refId": "B", "step": 4 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "每秒网络带宽使用$device", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bps", "label": "上传(-)/下载(+)", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "15分钟": "#6ED0E0", "1分钟": "#BF1B00", "5分钟": "#CCA300" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 1, "grid": {}, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 14 }, "height": "300", "hiddenSeries": false, "id": 13, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "maxPerRow": 6, "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "repeat": null, "seriesOverrides": [ { "alias": "/.*总核数/", "color": "#C4162A" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_load1{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "1分钟负载", "metric": "", "refId": "A", "step": 20, "target": "" }, { "expr": "node_load5{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "5分钟负载", "refId": "B", "step": 20 }, { "expr": "node_load15{env=\"$env\",instance=~\"$node\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "15分钟负载", "refId": "C", "step": 20 }, { "expr": " sum(count(node_cpu_seconds_total{env=\"$env\",instance=~\"$node\", mode='system'}) by (cpu,instance)) by(instance)", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "CPU总核数", "refId": "D", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "系统平均负载", "tooltip": { "msResolution": false, "shared": true, "sort": 2, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "vda_write": "#6ED0E0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "Read bytes 每个磁盘分区每秒读取的比特数\nWritten bytes 每个磁盘分区每秒写入的比特数", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 1, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 14 }, "height": "300", "hiddenSeries": false, "id": 168, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_读取$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_disk_read_bytes_total{env=\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_读取", "refId": "A", "step": 10 }, { "expr": "rate(node_disk_written_bytes_total{env=\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_写入", "refId": "B", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "每秒磁盘读写容量", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "Bps", "label": "读取(-)/写入(+)", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 1, "description": "", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 0, "fillGradient": 0, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 14 }, "hiddenSeries": false, "id": 174, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/Inodes.*/", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "(node_filesystem_size_bytes{env=~\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{env=~\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}) *100/(node_filesystem_avail_bytes {env=~\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}+(node_filesystem_size_bytes{env=~\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}-node_filesystem_free_bytes{env=~\"$env\",instance=~'$node',fstype=~\"ext.*|xfs\",mountpoint !~\".*pod.*\"}))", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{mountpoint}}", "refId": "A" }, { "expr": "node_filesystem_files_free{env=~\"$env\",instance=~'$node',fstype=~\"ext.?|xfs\"} / node_filesystem_files{env=~\"$env\",instance=~'$node',fstype=~\"ext.?|xfs\"}", "hide": true, "interval": "", "legendFormat": "Inodes:{{instance}}:{{mountpoint}}", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "磁盘使用率", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "percent", "label": "", "logBase": 1, "max": "100", "min": "0", "show": true }, { "decimals": 2, "format": "percentunit", "label": null, "logBase": 1, "max": "1", "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "vda_write": "#6ED0E0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "Reads completed: 每个磁盘分区每秒读完成次数\n\nWrites completed: 每个磁盘分区每秒写完成次数\n\nIO now 每个磁盘分区每秒正在处理的输入/输出请求数", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 0, "fillGradient": 0, "gridPos": { "h": 9, "w": 8, "x": 0, "y": 22 }, "height": "300", "hiddenSeries": false, "id": 161, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*_读取$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_disk_reads_completed_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_读取", "refId": "A", "step": 10 }, { "expr": "rate(node_disk_writes_completed_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_写入", "refId": "B", "step": 10 }, { "expr": "node_disk_io_now{env=~\"$env\",instance=~\"$node\"}", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "磁盘读写速率(IOPS)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "iops", "label": "读取(-)/写入(+)I/O ops/sec", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "Idle - Waiting for something to happen": "#052B51", "guest": "#9AC48A", "idle": "#052B51", "iowait": "#EAB839", "irq": "#BF1B00", "nice": "#C15C17", "sdb_每秒I/O操作%": "#d683ce", "softirq": "#E24D42", "steal": "#FCE2DE", "system": "#508642", "user": "#5195CE", "磁盘花费在I/O操作占比": "#ba43a9" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "description": "每一秒钟的自然时间内,花费在I/O上的耗时。(wall-clock time)\n\nnode_disk_io_time_seconds_total:\n磁盘花费在输入/输出操作上的秒数。该值为累加值。(Milliseconds Spent Doing I/Os)\n\nrate(node_disk_io_time_seconds_total[1m]):\n计算每秒的速率:(last值-last前一个值)/时间戳差值,即:1秒钟内磁盘花费在I/O操作的时间占比。", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 8, "x": 8, "y": 22 }, "hiddenSeries": false, "id": 175, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "rightSide": false, "show": true, "sideWidth": null, "sort": null, "sortDesc": null, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "maxPerRow": 6, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_disk_io_time_seconds_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_每秒I/O操作%", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "每1秒内I/O操作耗时占比", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "percentunit", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "vda": "#6ED0E0" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "Read time seconds 每个磁盘分区读操作花费的秒数\n\nWrite time seconds 每个磁盘分区写操作花费的秒数\n\nIO time seconds 每个磁盘分区输入/输出操作花费的秒数\n\nIO time weighted seconds每个磁盘分区输入/输出操作花费的加权秒数", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 1, "gridPos": { "h": 9, "w": 8, "x": 16, "y": 22 }, "height": "300", "hiddenSeries": false, "id": 160, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/,*_读取$/", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(node_disk_read_time_seconds_total{env=~\"$env\",instance=~\"$node\"}[$interval]) / rate(node_disk_reads_completed_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_读取", "refId": "B" }, { "expr": "rate(node_disk_write_time_seconds_total{env=~\"$env\",instance=~\"$node\"}[$interval]) / rate(node_disk_writes_completed_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_写入", "refId": "C" }, { "expr": "rate(node_disk_io_time_seconds_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}", "refId": "A", "step": 10 }, { "expr": "rate(node_disk_io_time_weighted_seconds_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{device}}_加权", "refId": "D" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "每次IO读写的耗时(参考:小于100ms)(beta)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": "读取(-)/写入(+)", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "192.168.200.241:9100_TCP_alloc": "semi-dark-blue", "TCP": "#6ED0E0", "TCP_alloc": "blue" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "Sockets_used - 已使用的所有协议套接字总量\n\nCurrEstab - 当前状态为 ESTABLISHED 或 CLOSE-WAIT 的 TCP 连接数\n\nTCP_alloc - 已分配(已建立、已申请到sk_buff)的TCP套接字数量\n\nTCP_tw - 等待关闭的TCP连接数\n\nUDP_inuse - 正在使用的 UDP 套接字数量\n\nRetransSegs - TCP 重传报文数\n\nOutSegs - TCP 发送的报文数\n\nInSegs - TCP 接收的报文数", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 0, "fillGradient": 0, "gridPos": { "h": 8, "w": 16, "x": 0, "y": 31 }, "height": "300", "hiddenSeries": false, "id": 158, "interval": "", "legend": { "alignAsTable": true, "avg": false, "current": true, "hideEmpty": true, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "sideWidth": null, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/.*Sockets_used/", "color": "#E02F44", "lines": false, "pointradius": 1, "points": true, "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_netstat_Tcp_CurrEstab{env=~\"$env\",instance=~'$node'}", "format": "time_series", "hide": false, "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "CurrEstab", "refId": "A", "step": 20 }, { "expr": "node_sockstat_TCP_tw{env=~\"$env\",instance=~'$node'}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TCP_tw", "refId": "D" }, { "expr": "node_sockstat_sockets_used{env=~\"$env\",instance=~'$node'}", "hide": false, "interval": "30m", "intervalFactor": 1, "legendFormat": "Sockets_used", "refId": "B" }, { "expr": "node_sockstat_UDP_inuse{env=~\"$env\",instance=~'$node'}", "interval": "", "legendFormat": "UDP_inuse", "refId": "C" }, { "expr": "node_sockstat_TCP_alloc{env=~\"$env\",instance=~'$node'}", "interval": "", "legendFormat": "TCP_alloc", "refId": "E" }, { "expr": "rate(node_netstat_Tcp_PassiveOpens{env=~\"$env\",instance=~'$node'}[$interval])", "hide": true, "interval": "", "legendFormat": "{{instance}}_Tcp_PassiveOpens", "refId": "G" }, { "expr": "rate(node_netstat_Tcp_ActiveOpens{env=~\"$env\",instance=~'$node'}[$interval])", "hide": true, "interval": "", "legendFormat": "{{instance}}_Tcp_ActiveOpens", "refId": "F" }, { "expr": "rate(node_netstat_Tcp_InSegs{env=~\"$env\",instance=~'$node'}[$interval])", "interval": "", "legendFormat": "Tcp_InSegs", "refId": "H" }, { "expr": "rate(node_netstat_Tcp_OutSegs{env=~\"$env\",instance=~'$node'}[$interval])", "interval": "", "legendFormat": "Tcp_OutSegs", "refId": "I" }, { "expr": "rate(node_netstat_Tcp_RetransSegs{env=~\"$env\",instance=~'$node'}[$interval])", "hide": false, "interval": "", "legendFormat": "Tcp_RetransSegs", "refId": "J" }, { "expr": "rate(node_netstat_TcpExt_ListenDrops{env=~\"$env\",instance=~'$node'}[$interval])", "hide": true, "interval": "", "legendFormat": "", "refId": "K" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "网络Socket连接信息", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "transformations": [], "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": "已使用的所有协议套接字总量", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "filefd_192.168.200.241:9100": "super-light-green", "switches_192.168.200.241:9100": "semi-dark-red", "使用的文件描述符_10.118.72.128:9100": "red", "每秒上下文切换次数_10.118.71.245:9100": "yellow", "每秒上下文切换次数_10.118.72.128:9100": "yellow" }, "bars": false, "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 0, "fillGradient": 1, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 31 }, "hiddenSeries": false, "hideTimeOverride": false, "id": 16, "legend": { "alignAsTable": false, "avg": false, "current": true, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 1, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/每秒上下文切换次数.*/", "color": "#FADE2A", "lines": false, "pointradius": 1, "points": true, "yaxis": 2 }, { "alias": "/使用的文件描述符.*/", "color": "#F2495C" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "node_filefd_allocated{env=~\"$env\",instance=~\"$node\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 5, "legendFormat": "使用的文件描述符", "refId": "B" }, { "expr": "rate(node_context_switches_total{env=~\"$env\",instance=~\"$node\"}[$interval])", "interval": "", "intervalFactor": 5, "legendFormat": "每秒上下文切换次数", "refId": "A" }, { "expr": " (node_filefd_allocated{env=~\"$env\",instance=~\"$node\"}/node_filefd_maximum{env=~\"$env\",instance=~\"$node\"}) *100", "format": "time_series", "hide": true, "instant": false, "interval": "", "intervalFactor": 5, "legendFormat": "使用的文件描述符占比_{{instance}}", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "打开的文件描述符(左 )/每秒上下文切换次数(右)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "使用的文件描述符", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": "context_switches", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "allValue": "", "current": {}, "datasource": "Prometheus", "definition": "label_values(origin_prometheus)", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "数据源", "multi": false, "name": "origin_prometheus", "options": [], "query": { "query": "label_values(origin_prometheus)", "refId": "Prometheus-origin_prometheus-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 5, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\"}, job)", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "JOB", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\"}, job)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 5, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\"}, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\"}, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\",job=~\"$job\",env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "multiFormat": "regex values", "name": "node", "options": [], "query": { "query": "label_values(node_uname_info{origin_prometheus=~\"$origin_prometheus\",job=~\"$job\",env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 5, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(node_network_info{origin_prometheus=~\"$origin_prometheus\",device!~'tap.*|veth.*|br.*|docker.*|virbr.*|lo.*|cni.*'},device)", "description": null, "error": null, "hide": 2, "includeAll": true, "label": "网卡", "multi": false, "multiFormat": "regex values", "name": "device", "options": [], "query": { "query": "label_values(node_network_info{origin_prometheus=~\"$origin_prometheus\",device!~'tap.*|veth.*|br.*|docker.*|virbr.*|lo.*|cni.*'},device)", "refId": "Prometheus-device-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "auto": false, "auto_count": 100, "auto_min": "10s", "current": { "selected": false, "text": "2m", "value": "2m" }, "datasource": null, "description": null, "error": null, "hide": 2, "label": "时间间隔", "name": "interval", "options": [ { "selected": false, "text": "30s", "value": "30s" }, { "selected": false, "text": "1m", "value": "1m" }, { "selected": true, "text": "2m", "value": "2m" }, { "selected": false, "text": "3m", "value": "3m" }, { "selected": false, "text": "5m", "value": "5m" }, { "selected": false, "text": "10m", "value": "10m" }, { "selected": false, "text": "30m", "value": "30m" } ], "query": "30s,1m,2m,3m,5m,10m,30m", "queryValue": "", "refresh": 2, "skipUrlSync": false, "type": "interval" }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "query_result(topk(1,sort_desc (max(node_filesystem_size_bytes{origin_prometheus=~\"$origin_prometheus\",instance=~'$node',fstype=~\"ext.?|xfs\",mountpoint!~\".*pods.*\"}) by (mountpoint))))", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "最大挂载目录", "multi": false, "name": "maxmount", "options": [], "query": { "query": "query_result(topk(1,sort_desc (max(node_filesystem_size_bytes{origin_prometheus=~\"$origin_prometheus\",instance=~'$node',fstype=~\"ext.?|xfs\",mountpoint!~\".*pods.*\"}) by (mountpoint))))", "refId": "Prometheus-maxmount-Variable-Query" }, "refresh": 2, "regex": "/.*\\\"(.*)\\\".*/", "skipUrlSync": false, "sort": 5, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/10-ignite-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 10, "uid": "m7a3BRPMk", "title": "Ignite 信息面板", "panels": [ { "datasource": "Prometheus", "description": "up time", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "ms" }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, "id": 2, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "up_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "UpTime ", "type": "stat" }, { "datasource": "Prometheus", "description": "the num of started thread", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, "id": 4, "options": { "displayMode": "gradient", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "ignite_started_thread_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "IgniteStartedThreadCount", "type": "bargauge" }, { "datasource": "Prometheus", "description": "num of sent messages", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "id": 30, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "sent_messages_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "SentMessagesCount", "type": "stat" }, { "datasource": "Prometheus", "description": "num of received messages", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, "id": 32, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "ignite_received_messages_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "IgniteReceivedMessagesCount", "type": "stat" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "average time of job wait", "fieldConfig": { "defaults": { "custom": {}, "unit": "s" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 4 }, "hiddenSeries": false, "id": 28, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "average_job_wait_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "AverageJobWaitTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "time of current job wait", "fieldConfig": { "defaults": { "custom": {}, "unit": "s" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 4 }, "hiddenSeries": false, "id": 26, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_job_wait_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CurrentJobWaitTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "max time of job wait", "fieldConfig": { "defaults": { "custom": {}, "unit": "s" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, "hiddenSeries": false, "id": 24, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "maximum_job_wait_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MaximumJobWaitTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "average time of job execute", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, "hiddenSeries": false, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "average_job_execute_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "AverageJobExecuteTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "time of current job executed", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 20 }, "hiddenSeries": false, "id": 20, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_job_execute_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CurrentJobExecuteTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "max time of job execute", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 20 }, "hiddenSeries": false, "id": 18, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "maximum_job_execute_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MaximumJobExecuteTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "busy time percentage", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 28 }, "hiddenSeries": false, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "busy_time_percentage{env=~\"$env\",instance=\"$instance\",job=\"$job\"}*100", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "BusyTimePercentage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "total busy time", "fieldConfig": { "defaults": { "custom": {}, "unit": "ms" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 28 }, "hiddenSeries": false, "id": 14, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(total_busy_time{env=~\"$env\",instance=\"$instance\",job=\"$job\"}[5m])", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "IgniteBusyTimeTotal", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "total idle time", "fieldConfig": { "defaults": { "custom": {}, "unit": "ms" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 36 }, "hiddenSeries": false, "id": 12, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(ignite_idle_time_total{env=~\"$env\",instance=\"$instance\",job=\"$job\"}[5m])", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "IgniteIdleTimeTotal", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "current daemon thread count", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 36 }, "hiddenSeries": false, "id": 10, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_daemon_thread_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CurrentDaemonThreadCount", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "maximum thread count", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 44 }, "hiddenSeries": false, "id": 8, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "maximum_thread_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MaximumThreadCount", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "current thread count", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 44 }, "hiddenSeries": false, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_thread_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CurrentThreadCount", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(ignite_started_thread_count,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(ignite_started_thread_count,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(ignite_started_thread_count{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instancce", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(ignite_started_thread_count{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "datasource": null, "description": null, "error": null, "hide": 2, "label": "job", "name": "job", "query": "igniteExporter", "skipUrlSync": false, "type": "constant" } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/11-kafka-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 11, "uid": "jwPKIsniz", "title": "Kafka 信息面板", "panels": [ { "cacheTimeout": null, "datasource": "Prometheus", "description": "kafka brokers", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "id": 20, "interval": null, "links": [], "options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "kafka_brokers{env=~\"$env\",instance=~\"$instance\",job=~\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "KafkaBrokers", "type": "stat" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "process open fds", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "process_open_fds{env=~\"$env\",instance=~\"$instance\",job=\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ProcessOpenFds", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Resident memory size ", "fieldConfig": { "defaults": { "custom": {}, "unit": "bytes" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "hiddenSeries": false, "id": 28, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "process_resident_memory_bytes{env=~\"$env\",instance=~\"$instance\",job=\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ProcessResidentMemoryBytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "datasource": "Prometheus", "description": "Total user and system CPU time spent in seconds", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "id": 26, "options": { "displayMode": "gradient", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_cpu_seconds_total{env=~\"$env\",instance=~\"$instance\",job=\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "ProcessCpuSecondsTotal", "type": "bargauge" }, { "aliasColors": {}, "bars": false, "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Number of partitions for this Topic", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 16 }, "hiddenSeries": false, "id": 24, "interval": null, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": false }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "kafka_topic_partitions{env=~\"$env\",instance=~\"$instance\",job=\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "kafkaTopicPartitions", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "kafka topic partition current offset", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 0, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 23 }, "hiddenSeries": false, "id": 16, "legend": { "alignAsTable": false, "avg": false, "current": true, "max": true, "min": false, "rightSide": true, "show": true, "sideWidth": 500, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(delta(kafka_topic_partition_current_offset{env=~\"$env\",instance=~'$instance'}[5m])/5) by (topic)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{topic}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "KafkaTopicPartitionCurrentOffset", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Number of Replicas for this Topic/Partition", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 31 }, "hiddenSeries": false, "id": 30, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "kafka_topic_partition_replicas{env=~\"$env\",instance=~\"$instance\",job=\"kafkaExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "KafkaTopicPartitionReplicas", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 24, "x": 0, "y": 39 }, "hiddenSeries": false, "id": 8, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sideWidth": 420, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum by(topic) (kafka_topic_partitions{env=~\"$env\",instance=\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{topic}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Partitions per Topic", "tooltip": { "shared": false, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "series", "name": null, "show": false, "values": [ "current" ] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 }, "hiddenSeries": false, "id": 32, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(kafka_consumergroup_lag{env=~\"$env$\",instance=~\"$instance$\"}) by (consumergroup)", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Consumergroup", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(kafka_brokers, job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(kafka_brokers, job)", "refId": "Prometheus-job-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(kafka_brokers, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(kafka_brokers, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(kafka_brokers{job=~\"$job\",env=\"$env\"}, instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(kafka_brokers{job=~\"$job\",env=\"$env\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/12-mysql-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 12, "uid": "MQWgroiiz", "title": "MySQL 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 382, "panels": [], "repeat": null, "title": "", "type": "row" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 1, "description": "**MySQL Uptime**\n\nThe amount of time since the last restart of the MySQL server process.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "s", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 }, "height": "125px", "id": 12, "interval": "", "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "s", "postfixFontSize": "80%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_uptime{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "300,3600", "title": "MySQL Uptime", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 2, "description": "**Current QPS**\n\nBased on the queries reported by MySQL's ``SHOW STATUS`` command, it is the number of statements executed by the server within the last second. This variable includes statements executed within stored programs, unlike the Questions variable. It does not count \n``COM_PING`` or ``COM_STATISTICS`` commands.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 }, "height": "125px", "id": 13, "interval": "", "links": [ { "targetBlank": true, "title": "MySQL Server Status Variables", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Queries" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_queries{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_queries{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "metric": "", "refId": "A", "step": 20 } ], "thresholds": "35,75", "title": "Current QPS", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "datasource": "Prometheus", "decimals": 0, "description": "**InnoDB Buffer Pool Size**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 }, "height": "125px", "id": 51, "interval": "", "links": [ { "targetBlank": true, "title": "Tuning the InnoDB Buffer Pool Size", "url": "https://www.percona.com/blog/2015/06/02/80-ram-tune-innodb_buffer_pool_size/" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_innodb_buffer_pool_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "90,95", "title": "InnoDB Buffer Pool Size", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "description": "**InnoDB Buffer Pool Size % of Total RAM**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "percent", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 }, "height": "125px", "id": 52, "interval": "", "links": [ { "targetBlank": true, "title": "Tuning the InnoDB Buffer Pool Size", "url": "https://www.percona.com/blog/2015/06/02/80-ram-tune-innodb_buffer_pool_size/" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "repeat": null, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "(mysql_global_variables_innodb_buffer_pool_size{env=\"$env\",instance=\"$host\"} * 100) / on (instance) node_memory_MemTotal{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "40,80", "title": "Buffer Pool Size of Total RAM", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 383, "panels": [], "repeat": null, "title": "Connections", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "description": "**Max Connections** \n\nMax Connections is the maximum permitted number of simultaneous client connections. By default, this is 151. Increasing this value increases the number of file descriptors that mysqld requires. If the required number of descriptors are not available, the server reduces the value of Max Connections.\n\nmysqld actually permits Max Connections + 1 clients to connect. The extra connection is reserved for use by accounts that have the SUPER privilege, such as root.\n\nMax Used Connections is the maximum number of connections that have been in use simultaneously since the server started.\n\nConnections is the number of connection attempts (successful or not) to the MySQL server.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 }, "height": "250px", "hiddenSeries": false, "id": 92, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "targetBlank": true, "title": "MySQL Server System Variables", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_connections" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Max Connections", "fill": 0 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max(max_over_time(mysql_global_status_threads_connected{env=\"$env\",instance=\"$host\"}[5m]) or mysql_global_status_threads_connected{env=\"$env\",instance=\"$host\"} )", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Connections", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_max_used_connections{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Max Used Connections", "metric": "", "refId": "C", "step": 20, "target": "" }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_max_connections{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Max Connections", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Active Threads**\n\nThreads Connected is the number of open connections, while Threads Running is the number of threads not sleeping.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 6 }, "hiddenSeries": false, "id": 10, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": false, "show": true, "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Peak Threads Running", "color": "#E24D42", "lines": false, "pointradius": 1, "points": true }, { "alias": "Peak Threads Connected", "color": "#1F78C1" }, { "alias": "Avg Threads Running", "color": "#EAB839" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max_over_time(mysql_global_status_threads_connected{env=\"$env\",instance=\"$host\"}[5m]) or\nmax_over_time(mysql_global_status_threads_connected{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Peak Threads Connected", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max_over_time(mysql_global_status_threads_running{env=\"$env\",instance=\"$host\"}[5m]) or\nmax_over_time(mysql_global_status_threads_running{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Peak Threads Running", "metric": "", "refId": "B", "step": 20 }, { "expr": "avg_over_time(mysql_global_status_threads_running{env=\"$env\",instance=\"$host\"}[5m]) or \navg_over_time(mysql_global_status_threads_running{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Avg Threads Running", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Client Thread Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ "total" ] }, "yaxes": [ { "format": "short", "label": "Threads", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 }, "id": 384, "panels": [], "repeat": null, "title": "Table Locks", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "description": "**MySQL Questions**\n\nThe number of statements executed by the server. This includes only statements sent to the server by clients and not statements executed within stored programs, unlike the Queries used in the QPS calculation. \n\nThis variable does not count the following commands:\n* ``COM_PING``\n* ``COM_STATISTICS``\n* ``COM_STMT_PREPARE``\n* ``COM_STMT_CLOSE``\n* ``COM_STMT_RESET``", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 53, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "targetBlank": true, "title": "MySQL Queries and Questions", "url": "https://www.percona.com/blog/2014/05/29/how-mysql-queries-and-questions-are-measured/" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_questions{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_questions{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Questions", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Questions", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Thread Cache**\n\nThe thread_cache_size variable sets how many threads the server should cache to reuse. When a client disconnects, the client's threads are put in the cache if the cache is not full. It is autosized in MySQL 5.6.8 and above (capped to 100). Requests for threads are satisfied by reusing threads taken from the cache if possible, and only when the cache is empty is a new thread created.\n\n* *Threads_created*: The number of threads created to handle connections.\n* *Threads_cached*: The number of threads in the thread cache.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 11, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Tuning information", "url": "https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_thread_cache_size" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Threads Created", "fill": 0 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_thread_cache_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Thread Cache Size", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_threads_cached{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Threads Cached", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_threads_created{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_threads_created{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Threads Created", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Thread Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 21 }, "id": 385, "panels": [], "repeat": null, "title": "Temporary Objects", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 22 }, "hiddenSeries": false, "id": 22, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_tables{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_created_tmp_tables{env=\"$env\",instance=\"$host\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Tables", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_disk_tables{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_created_tmp_disk_tables{env=\"$env\",instance=\"$host\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Disk Tables", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_files{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_created_tmp_files{env=\"$env\",instance=\"$host\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Files", "metric": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Temporary Objects", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Select Types**\n\nAs with most relational databases, selecting based on indexes is more efficient than scanning an entire table's data. Here we see the counters for selects not done with indexes.\n\n* ***Select Scan*** is how many queries caused full table scans, in which all the data in the table had to be read and either discarded or returned.\n* ***Select Range*** is how many queries used a range scan, which means MySQL scanned all rows in a given range.\n* ***Select Full Join*** is the number of joins that are not joined on an index, this is usually a huge performance hit.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 22 }, "height": "250px", "hiddenSeries": false, "id": 311, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_full_join{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_select_full_join{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Full Join", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_full_range_join{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_select_full_range_join{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Full Range Join", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_range{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_select_range{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Range", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_range_check{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_select_range_check{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Range Check", "metric": "", "refId": "D", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_scan{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_select_scan{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Scan", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Select Types", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 29 }, "id": 386, "panels": [], "repeat": null, "title": "Sorts", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Sorts**\n\nDue to a query's structure, order, or other requirements, MySQL sorts the rows before returning them. For example, if a table is ordered 1 to 10 but you want the results reversed, MySQL then has to sort the rows to return 10 to 1.\n\nThis graph also shows when sorts had to scan a whole table or a given range of a table in order to return the results and which could not have been sorted via an index.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 30 }, "hiddenSeries": false, "id": 30, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_rows{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_sort_rows{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Rows", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_range{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_sort_range{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Range", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_merge_passes{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_sort_merge_passes{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Merge Passes", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_scan{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_sort_scan{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Scan", "metric": "", "refId": "D", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Sorts", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Slow Queries**\n\nSlow queries are defined as queries being slower than the long_query_time setting. For example, if you have long_query_time set to 3, all queries that take longer than 3 seconds to complete will show on this graph.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 30 }, "hiddenSeries": false, "id": 48, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_slow_queries{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_slow_queries{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Slow Queries", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Slow Queries", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 37 }, "id": 387, "panels": [], "repeat": null, "title": "Aborted", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Aborted Connections**\n\nWhen a given host connects to MySQL and the connection is interrupted in the middle (for example due to bad credentials), MySQL keeps that info in a system table (since 5.6 this table is exposed in performance_schema).\n\nIf the amount of failed requests without a successful connection reaches the value of max_connect_errors, mysqld assumes that something is wrong and blocks the host from further connection.\n\nTo allow connections from that host again, you need to issue the ``FLUSH HOSTS`` statement.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 38 }, "hiddenSeries": false, "id": 47, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_aborted_connects{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_aborted_connects{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Aborted Connects (attempts)", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_aborted_clients{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_aborted_clients{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Aborted Clients (timeout)", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Aborted Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Table Locks**\n\nMySQL takes a number of different locks for varying reasons. In this graph we see how many Table level locks MySQL has requested from the storage engine. In the case of InnoDB, many times the locks could actually be row locks as it only takes table level locks in a few specific cases.\n\nIt is most useful to compare Locks Immediate and Locks Waited. If Locks waited is rising, it means you have lock contention. Otherwise, Locks Immediate rising and falling is normal activity.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 38 }, "hiddenSeries": false, "id": 32, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_table_locks_immediate{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_locks_immediate{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Locks Immediate", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_table_locks_waited{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_locks_waited{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Locks Waited", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Locks", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 45 }, "id": 388, "panels": [], "repeat": null, "title": "Network", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Network Traffic**\n\nHere we can see how much network traffic is generated by MySQL. Outbound is network traffic sent from MySQL and Inbound is network traffic MySQL has received.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 46 }, "hiddenSeries": false, "id": 9, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_bytes_received{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_bytes_received{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inbound", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_bytes_sent{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_bytes_sent{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Outbound", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Network Traffic", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Network Usage Hourly**\n\nHere we can see how much network traffic is generated by MySQL per hour. You can use the bar graph to compare data sent by MySQL and data received by MySQL.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 46 }, "height": "250px", "hiddenSeries": false, "id": 381, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "increase(mysql_global_status_bytes_received{env=\"$env\",instance=\"$host\"}[1h])", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Received", "metric": "", "refId": "A", "step": 3600 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "increase(mysql_global_status_bytes_sent{env=\"$env\",instance=\"$host\"}[1h])", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Sent", "metric": "", "refId": "B", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "MySQL Network Usage Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 53 }, "id": 389, "panels": [], "repeat": null, "title": "Memory", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "description": "***System Memory***: Total Memory for the system.\\\n***InnoDB Buffer Pool Data***: InnoDB maintains a storage area called the buffer pool for caching data and indexes in memory.\\\n***TokuDB Cache Size***: Similar in function to the InnoDB Buffer Pool, TokuDB will allocate 50% of the installed RAM for its own cache.\\\n***Key Buffer Size***: Index blocks for MYISAM tables are buffered and are shared by all threads. key_buffer_size is the size of the buffer used for index blocks.\\\n***Adaptive Hash Index Size***: When InnoDB notices that some index values are being accessed very frequently, it builds a hash index for them in memory on top of B-Tree indexes.\\\n ***Query Cache Size***: The query cache stores the text of a SELECT statement together with the corresponding result that was sent to the client. The query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time.\\\n***InnoDB Dictionary Size***: The data dictionary is InnoDB ‘s internal catalog of tables. InnoDB stores the data dictionary on disk, and loads entries into memory while the server is running.\\\n***InnoDB Log Buffer Size***: The MySQL InnoDB log buffer allows transactions to run without having to write the log to disk before the transactions commit.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 54 }, "hiddenSeries": false, "id": 50, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Detailed descriptions about metrics", "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "System Memory", "fill": 0, "stack": false } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "node_memory_MemTotal{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "System Memory", "refId": "G", "step": 4 }, { "expr": "mysql_global_status_innodb_page_size{env=\"$env\",instance=\"$host\"} * on (instance) mysql_global_status_buffer_pool_pages{env=\"$env\",instance=\"$host\",state=\"data\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Buffer Pool Data", "refId": "A", "step": 20 }, { "expr": "mysql_global_variables_innodb_log_buffer_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Log Buffer Size", "refId": "D", "step": 20 }, { "expr": "mysql_global_variables_innodb_additional_mem_pool_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "InnoDB Additional Memory Pool Size", "refId": "H", "step": 40 }, { "expr": "mysql_global_status_innodb_mem_dictionary{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Dictionary Size", "refId": "F", "step": 20 }, { "expr": "mysql_global_variables_key_buffer_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Key Buffer Size", "refId": "B", "step": 20 }, { "expr": "mysql_global_variables_query_cache_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Query Cache Size", "refId": "C", "step": 20 }, { "expr": "mysql_global_status_innodb_mem_adaptive_hash{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Adaptive Hash Index Size", "refId": "E", "step": 20 }, { "expr": "mysql_global_variables_tokudb_cache_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TokuDB Cache Size", "refId": "I", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Internal Memory Overview", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 61 }, "id": 390, "panels": [], "repeat": null, "title": "Command, Handlers, Processes", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Top Command Counters**\n\nThe Com_{{xxx}} statement counter variables indicate the number of times each xxx statement has been executed. There is one status variable for each type of statement. For example, Com_delete and Com_update count [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements, respectively. Com_delete_multi and Com_update_multi are similar but apply to [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements that use multiple-table syntax.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 62 }, "hiddenSeries": false, "id": 14, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideEmpty": false, "hideZero": false, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (Com_xxx)", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Com_xxx" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, rate(mysql_global_status_commands_total{env=\"$env\",instance=\"$host\"}[5m])>0) or topk(5, irate(mysql_global_status_commands_total{env=\"$env\",instance=\"$host\"}[5m])>0)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Com_{{ command }}", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Top Command Counters", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Top Command Counters Hourly**\n\nThe Com_{{xxx}} statement counter variables indicate the number of times each xxx statement has been executed. There is one status variable for each type of statement. For example, Com_delete and Com_update count [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements, respectively. Com_delete_multi and Com_update_multi are similar but apply to [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements that use multiple-table syntax.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 69 }, "hiddenSeries": false, "id": 39, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [ { "title": "Server Status Variables (Com_xxx)", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Com_xxx" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, increase(mysql_global_status_commands_total{env=\"$env\",instance=\"$host\"}[1h])>0)", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Com_{{ command }}", "metric": "", "refId": "A", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "Top Command Counters Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Handlers**\n\nHandler statistics are internal statistics on how MySQL is selecting, updating, inserting, and modifying rows, tables, and indexes.\n\nThis is in fact the layer between the Storage Engine and MySQL.\n\n* `read_rnd_next` is incremented when the server performs a full table scan and this is a counter you don't really want to see with a high value.\n* `read_key` is incremented when a read is done with an index.\n* `read_next` is incremented when the storage engine is asked to 'read the next index entry'. A high value means a lot of index scans are being done.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 76 }, "hiddenSeries": false, "id": 8, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_handlers_total{env=\"$env\",instance=\"$host\", handler!~\"commit|rollback|savepoint.*|prepare\"}[5m]) or irate(mysql_global_status_handlers_total{env=\"$env\",instance=\"$host\", handler!~\"commit|rollback|savepoint.*|prepare\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ handler }}", "metric": "", "refId": "J", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Handlers", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 83 }, "hiddenSeries": false, "id": 28, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_handlers_total{env=\"$env\",instance=\"$host\", handler=~\"commit|rollback|savepoint.*|prepare\"}[5m]) or irate(mysql_global_status_handlers_total{env=\"$env\",instance=\"$host\", handler=~\"commit|rollback|savepoint.*|prepare\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "{{ handler }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Transaction Handlers", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 0, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 90 }, "hiddenSeries": false, "id": 40, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_info_schema_threads{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "{{ state }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Process States", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 97 }, "hiddenSeries": false, "id": 49, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, avg_over_time(mysql_info_schema_threads{env=\"$env\",instance=\"$host\"}[1h]))", "interval": "1h", "intervalFactor": 1, "legendFormat": "{{ state }}", "metric": "", "refId": "A", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "Top Process States Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 104 }, "id": 391, "panels": [], "repeat": null, "title": "Query Cache", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Query Cache Memory**\n\nThe query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time. This serialization is true not only for SELECTs, but also for INSERT/UPDATE/DELETE.\n\nThis also means that the larger the `query_cache_size` is set to, the slower those operations become. In concurrent environments, the MySQL Query Cache quickly becomes a contention point, decreasing performance. MariaDB and AWS Aurora have done work to try and eliminate the query cache contention in their flavors of MySQL, while MySQL 8.0 has eliminated the query cache feature.\n\nThe recommended settings for most environments is to set:\n ``query_cache_type=0``\n ``query_cache_size=0``\n\nNote that while you can dynamically change these values, to completely remove the contention point you have to restart the database.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 105 }, "hiddenSeries": false, "id": 46, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_qcache_free_memory{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Free Memory", "metric": "", "refId": "F", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_query_cache_size{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Query Cache Size", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Query Cache Memory", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Query Cache Activity**\n\nThe query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time. This serialization is true not only for SELECTs, but also for INSERT/UPDATE/DELETE.\n\nThis also means that the larger the `query_cache_size` is set to, the slower those operations become. In concurrent environments, the MySQL Query Cache quickly becomes a contention point, decreasing performance. MariaDB and AWS Aurora have done work to try and eliminate the query cache contention in their flavors of MySQL, while MySQL 8.0 has eliminated the query cache feature.\n\nThe recommended settings for most environments is to set:\n``query_cache_type=0``\n``query_cache_size=0``\n\nNote that while you can dynamically change these values, to completely remove the contention point you have to restart the database.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 105 }, "height": "", "hiddenSeries": false, "id": 45, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_hits{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_qcache_hits{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Hits", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_inserts{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_qcache_inserts{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inserts", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_not_cached{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_qcache_not_cached{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Not Cached", "metric": "", "refId": "D", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_lowmem_prunes{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_qcache_lowmem_prunes{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Prunes", "metric": "", "refId": "F", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_qcache_queries_in_cache{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Queries in Cache", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Query Cache Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 112 }, "id": 392, "panels": [], "repeat": null, "title": "Files and Tables", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 113 }, "hiddenSeries": false, "id": 43, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_opened_files{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_opened_files{env=\"$env\",instance=\"$host\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Openings", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL File Openings", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 113 }, "hiddenSeries": false, "id": 41, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_files{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Open Files", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_open_files_limit{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Open Files Limit", "metric": "", "refId": "D", "step": 20 }, { "expr": "mysql_global_status_innodb_num_open_files{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Open Files", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Open Files", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 120 }, "id": 393, "panels": [], "repeat": null, "title": "Table Openings", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Table Open Cache Status**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 121 }, "hiddenSeries": false, "id": 44, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Table Open Cache Hit Ratio", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_opened_tables{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_opened_tables{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Openings", "metric": "", "refId": "A", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Hits", "refId": "B", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_misses{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_misses{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Misses", "refId": "C", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_overflows{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_overflows{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Misses due to Overflows", "refId": "D", "step": 20 }, { "expr": "(rate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m]))/((rate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{env=\"$env\",instance=\"$host\"}[5m]))+(rate(mysql_global_status_table_open_cache_misses{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_table_open_cache_misses{env=\"$env\",instance=\"$host\"}[5m])))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Open Cache Hit Ratio", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Open Cache Status", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "percentunit", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Open Tables**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 121 }, "hiddenSeries": false, "id": 42, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_tables{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Open Tables", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_table_open_cache{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Open Cache", "metric": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Open Tables", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 128 }, "id": 394, "panels": [], "repeat": null, "title": "MySQL Table Definition Cache", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Table Definition Cache**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 129 }, "hiddenSeries": false, "id": 54, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Opened Table Definitions", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_table_definitions{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Open Table Definitions", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_table_definition_cache{env=\"$env\",instance=\"$host\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Definitions Cache Size", "metric": "", "refId": "C", "step": 20 }, { "expr": "rate(mysql_global_status_opened_table_definitions{env=\"$env\",instance=\"$host\"}[5m]) or irate(mysql_global_status_opened_table_definitions{env=\"$env\",instance=\"$host\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Opened Table Definitions", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Definition Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 136 }, "id": 395, "panels": [], "repeat": null, "title": "System Charts", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 137 }, "hiddenSeries": false, "id": 31, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pgpgin{env=\"$env\",instance=\"$host\"}[5m]) * 1024 or irate(node_vmstat_pgpgin{env=\"$env\",instance=\"$host\"}[5m]) * 1024", "interval": "", "intervalFactor": 1, "legendFormat": "Page In", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pgpgout{env=\"$env\",instance=\"$host\"}[5m]) * 1024 or irate(node_vmstat_pgpgout{env=\"$env\",instance=\"$host\"}[5m]) * 1024", "interval": "", "intervalFactor": 1, "legendFormat": "Page Out", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "I/O Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 137 }, "height": "250px", "hiddenSeries": false, "id": 37, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_MemTotal{env=\"$env\",instance=\"$host\"} - (node_memory_MemFree{env=\"$env\",instance=\"$host\"} + node_memory_Buffers{env=\"$env\",instance=\"$host\"} + node_memory_Cached{env=\"$env\",instance=\"$host\"})", "interval": "", "intervalFactor": 1, "legendFormat": "Used", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_MemFree{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Free", "metric": "", "refId": "B", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_Buffers{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Buffers", "metric": "", "refId": "D", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_Cached{env=\"$env\",instance=\"$host\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Cached", "metric": "", "refId": "E", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Memory Distribution", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "Load 1m": "#58140C", "Max Core Utilization": "#bf1b00", "iowait": "#e24d42", "nice": "#1f78c1", "softirq": "#806eb7", "system": "#eab839", "user": "#508642" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 137 }, "height": "", "hiddenSeries": false, "id": 2, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Max Core Utilization", "lines": false, "pointradius": 1, "points": true, "stack": false }, { "alias": "Load 1m", "color": "#58140C", "fill": 2, "stack": false, "yaxis": 2 } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "clamp_max(((avg by (mode) ( (clamp_max(rate(node_cpu{env=\"$env\",instance=\"$host\",mode!=\"idle\"}[5m]),1)) or (clamp_max(irate(node_cpu{env=\"$env\",instance=\"$host\",mode!=\"idle\"}[5m]),1)) ))*100 or (avg_over_time(node_cpu_average{env=\"$env\",instance=~\"$host\", mode!=\"total\", mode!=\"idle\"}[5m]) or avg_over_time(node_cpu_average{env=\"$env\",instance=~\"$host\", mode!=\"total\", mode!=\"idle\"}[5m]))),100)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ mode }}", "metric": "", "refId": "A", "step": 20 }, { "expr": "clamp_max(max by () (sum by (cpu) ( (clamp_max(rate(node_cpu{env=\"$env\",instance=\"$host\",mode!=\"idle\",mode!=\"iowait\"}[5m]),1)) or (clamp_max(irate(node_cpu{env=\"$env\",instance=\"$host\",mode!=\"idle\",mode!=\"iowait\"}[5m]),1)) ))*100,100)", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "Max Core Utilization", "refId": "B", "step": 20 }, { "expr": "node_load1{env=\"$env\",instance=\"$host\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "Load 1m", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CPU Usage / Load", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 1, "format": "percent", "label": "", "logBase": 1, "max": 100, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 144 }, "height": "250px", "hiddenSeries": false, "id": 36, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 1, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "sum((rate(node_disk_read_time_ms{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m]) / rate(node_disk_reads_completed{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m])) or (irate(node_disk_read_time_ms{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m]) / irate(node_disk_reads_completed{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m]))\nor avg_over_time(aws_rds_read_latency_average{env=\"$env\",instance=\"$host\"}[5m]) or avg_over_time(aws_rds_read_latency_average{env=\"$env\",instance=\"$host\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Read", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "sum((rate(node_disk_write_time_ms{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m]) / rate(node_disk_writes_completed{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m])) or (irate(node_disk_write_time_ms{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m]) / irate(node_disk_writes_completed{device!~\"dm-.+\", env=\"$env\",instance=\"$host\"}[5m])) or \navg_over_time(aws_rds_write_latency_average{env=\"$env\",instance=\"$host\"}[5m]) or avg_over_time(aws_rds_write_latency_average{env=\"$env\",instance=\"$host\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Write", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Disk Latency", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": "", "logBase": 2, "max": null, "min": null, "show": true }, { "format": "ms", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 144 }, "height": "250px", "hiddenSeries": false, "id": 21, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Outbound", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "sum(rate(node_network_receive_bytes{env=\"$env\",instance=\"$host\", device!=\"lo\"}[5m])) or sum(irate(node_network_receive_bytes{env=\"$env\",instance=\"$host\", device!=\"lo\"}[5m])) or sum(max_over_time(rdsosmetrics_network_rx{env=\"$env\",instance=\"$host\"}[5m])) or sum(max_over_time(rdsosmetrics_network_rx{env=\"$env\",instance=\"$host\"}[5m])) ", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inbound", "metric": "", "refId": "B", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "sum(rate(node_network_transmit_bytes{env=\"$env\",instance=\"$host\", device!=\"lo\"}[5m])) or sum(irate(node_network_transmit_bytes{env=\"$env\",instance=\"$host\", device!=\"lo\"}[5m])) or\nsum(max_over_time(rdsosmetrics_network_tx{env=\"$env\",instance=\"$host\"}[5m])) or sum(max_over_time(rdsosmetrics_network_tx{env=\"$env\",instance=\"$host\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Outbound", "metric": "", "refId": "A", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Network Traffic", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 144 }, "hiddenSeries": false, "id": 38, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pswpin{env=\"$env\",instance=\"$host\"}[5m]) * 4096 or irate(node_vmstat_pswpin{env=\"$env\",instance=\"$host\"}[5m]) * 4096", "interval": "", "intervalFactor": 1, "legendFormat": "Swap In (Reads)", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pswpout{env=\"$env\",instance=\"$host\"}[5m]) * 4096 or irate(node_vmstat_pswpout{env=\"$env\",instance=\"$host\"}[5m]) * 4096", "interval": "", "intervalFactor": 1, "legendFormat": "Swap Out (Writes)", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Swap Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "auto": true, "auto_count": 200, "auto_min": "1s", "current": { "selected": false, "text": "auto", "value": "$__auto_interval_interval" }, "datasource": "Prometheus", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "Interval", "multi": false, "multiFormat": "glob", "name": "interval", "options": [ { "selected": true, "text": "auto", "value": "$__auto_interval_interval" }, { "selected": false, "text": "1s", "value": "1s" }, { "selected": false, "text": "5s", "value": "5s" }, { "selected": false, "text": "1m", "value": "1m" }, { "selected": false, "text": "5m", "value": "5m" }, { "selected": false, "text": "1h", "value": "1h" }, { "selected": false, "text": "6h", "value": "6h" }, { "selected": false, "text": "1d", "value": "1d" } ], "query": "1s,5s,1m,5m,1h,6h,1d", "queryValue": "", "refresh": 2, "skipUrlSync": false, "type": "interval" }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(mysql_up, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(mysql_up, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(mysql_up{env=\"$env\"}, instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Host", "multi": false, "multiFormat": "regex values", "name": "host", "options": [], "query": { "query": "label_values(mysql_up{env=\"$env\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "refresh_on_load": false, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": null, "tags": [], "tagsQuery": null, "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/13-nacos-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 13, "uid": "Bz_QALEiz1", "title": "Nacos 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 80, "panels": [], "title": "nacos monitor", "type": "row" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 0, "y": 1 }, "id": 89, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "count(nacos_monitor{name=\"configCount\",env=\"$env\",instance=\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "UP", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 3, "y": 1 }, "id": 90, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(nacos_monitor{name='serviceCount',env=\"$env\",instance=\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "service count", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 6, "y": 1 }, "id": 93, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(nacos_monitor{name='ipCount',env=\"$env\",instance=\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "ip count", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 9, "y": 1 }, "id": 92, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(nacos_monitor{name='configCount',env=\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "config count", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 12, "y": 1 }, "id": 91, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(nacos_monitor{name='longPolling',env=\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "long polling", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 15, "y": 1 }, "id": 88, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(nacos_monitor{name='getConfig',env=\"$env\",instance=~\"$instance\"}) by (name)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "config push total", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 18, "y": 1 }, "id": 82, "links": [], "options": { "content": "\n\n
\n\n\n\n
\n\n", "mode": "html" }, "pluginVersion": "7.4.3", "title": "", "type": "text" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 0, "y": 4 }, "id": 33, "interval": "", "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "%", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "repeat": null, "repeatDirection": "h", "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(system_cpu_usage{env=\"$env\",instance=~\"$instance\"}) * 100", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "50,80", "title": "cpu", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 70, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 9, "y": 4 }, "id": 32, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "%", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(jvm_memory_used_bytes{area=\"heap\", env=\"$env\",instance=~'$instance'})/sum(jvm_memory_max_bytes{area=\"heap\", env=~\"$env\",instance=~'$instance'}) * 100", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "50,70", "title": "memory", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "dashboardFilter": "", "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "folderId": null, "gridPos": { "h": 16, "w": 6, "x": 18, "y": 4 }, "id": 48, "limit": 10, "links": [], "nameFilter": "", "onlyAlertsOnDashboard": false, "repeat": null, "show": "current", "sortOrder": 1, "stateFilter": [], "title": "alert list", "type": "alertlist" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 1500, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 0, "y": 8 }, "id": 29, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(jvm_threads_daemon_threads{env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "800,1500", "title": "threads", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 20, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 9, "y": 8 }, "id": 30, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(system_load_average_1m{env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "5,10", "title": "load", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 5000, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 0, "y": 12 }, "id": 61, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "ms", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(rate(nacos_timer_seconds_sum{env=~\"$env\",instance=~'$instance'}[1m]))/sum(rate(nacos_timer_seconds_count{env=~\"$env\",instance=~'$instance'}[1m])) * 1000", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "3000,5000", "title": "notify rt", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 5000, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 9, "y": 12 }, "id": 26, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "ms", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(rate(http_server_requests_seconds_sum{env=~\"$env\",instance=~'$instance'}[1m]))/sum(rate(http_server_requests_seconds_count{env=~\"$env\",instance=~'$instance'}[1m])) * 1000", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "3000,5000", "title": "rt", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 2000, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 0, "y": 16 }, "id": 25, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(rate(http_server_requests_seconds_count{env=~\"$env\",instance=~'$instance'}[1m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "1000,2000", "title": "qps", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorPrefix": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 5000, "minValue": 0, "show": true, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 9, "x": 9, "y": 16 }, "id": 70, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "ms", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "max(nacos_monitor{name='avgPushCost', env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "1000,5000", "title": "avgPushCost", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 20 }, "id": 78, "panels": [], "title": "nacos detail", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 20, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(http_server_requests_seconds_sum{uri=~'/v1/cs/configs|/nacos/v1/ns', env=~\"$env\",instance=~'$instance'}[1m])/rate(http_server_requests_seconds_count{uri=~'/v1/cs/configs|/nacos/v1/ns/instance|/nacos/v1/ns/health', env=~\"$env\",instance=~'$instance'}[1m])) by (method,uri) * 1000", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" }, { "expr": "sum(rate(http_server_requests_seconds_sum{env=~\"$env\",instance=~'$instance'}[1m]))/sum(rate(http_server_requests_seconds_count{env=~\"$env\",instance=~'$instance'}[1m])) * 1000", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "all", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rt", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 21 }, "hiddenSeries": false, "id": 41, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "repeat": "group", "repeatDirection": "h", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='longPolling', env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "long polling", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 21 }, "hiddenSeries": false, "id": 37, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(system_load_average_1m{env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "load 1m", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 26 }, "hiddenSeries": false, "id": 18, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(http_server_requests_seconds_count{uri=~'/v1/cs/configs|/nacos/v1/ns/instance|/nacos/v1/ns/health', env=~\"$env\",instance=~'$instance'}[1m])) by (method,uri)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" }, { "expr": "sum(rate(http_server_requests_seconds_count{env=~\"$env\",instance=~'$instance'}[1m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "qps", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 26 }, "hiddenSeries": false, "id": 52, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='leaderStatus', env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "leaderStatus", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 26 }, "hiddenSeries": false, "id": 50, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='avgPushCost', env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "avgPushCost", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 31 }, "hiddenSeries": false, "id": 53, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(nacos_monitor{name='maxPushCost', env=~\"$env\",instance=~'$instance'})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "maxPushCost", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 31 }, "hiddenSeries": false, "id": 83, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='publish', env=~\"$env\",instance=~'$instance'}) by (name)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "publish config", "refId": "A" }, { "expr": "sum(nacos_monitor{name='getConfig', env=~\"$env\",instance=~'$instance'}) by (name)", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "get config", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "config statistics", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 31 }, "hiddenSeries": false, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(nacos_monitor{name=~'.*HealthCheck', env=~\"$env\",instance=~'$instance'}[1m])) by (name) * 60", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "health check", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 36 }, "id": 74, "panels": [], "title": "nacos alert", "type": "row" }, { "alert": { "conditions": [ { "evaluator": { "params": [ 50 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "for": "1m", "frequency": "1m", "handler": 1, "name": "cpu alert", "noDataState": "ok", "notifications": [ { "id": 1 } ] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 37 }, "hiddenSeries": false, "id": 45, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(system_cpu_usage{env=~\"$env\",instance=~\"$instance\"}) * 100", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 50, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "cpu alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 15 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "load 1m alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 37 }, "hiddenSeries": false, "id": 86, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(system_load_average_1m{env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 15, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "load alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 60 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "5m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "memory alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 37 }, "hiddenSeries": false, "id": 46, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(jvm_memory_used_bytes{area=\"heap\",env=~\"$env\",instance=~\"$instance\"})/sum(jvm_memory_max_bytes{area=\"heap\",env=~\"$env\",instance=~\"$instance\"}) * 100", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 60, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "memory alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 500 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "threads alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 42 }, "hiddenSeries": false, "id": 39, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(jvm_threads_daemon_threads{env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 500, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "threads alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 5 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "for": "1m", "frequency": "1m", "handler": 1, "message": "too many full gc", "name": "gc alert", "noDataState": "ok", "notifications": [ { "id": 1 } ] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 42 }, "hiddenSeries": false, "id": 38, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(rate(jvm_gc_pause_seconds_count{action=\"end of major GC\",env=~\"$env\",instance=~\"$instance\"}[5m])) * 300", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 5, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "gc alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 10 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "notify task alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 42 }, "hiddenSeries": false, "id": 49, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='notifyTask',env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 10, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "notify task alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 5000 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "B", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "rt alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 0, "y": 47 }, "hiddenSeries": false, "id": 85, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(http_server_requests_seconds_sum{env=~\"$env\",instance=~\"$instance\"}[1m]))/sum(rate(http_server_requests_seconds_count{env=~\"$env\",instance=~\"$instance\"}[1m])) * 1000", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "B" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 5000, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rt alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 5000 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "long polling alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 8, "y": 47 }, "hiddenSeries": false, "id": 84, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "repeatDirection": "h", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "max(nacos_monitor{name='longPolling',env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 5000, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "long polling alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "alert": { "conditions": [ { "evaluator": { "params": [ 1 ], "type": "gt" }, "operator": { "type": "and" }, "query": { "params": [ "A", "1m", "now" ] }, "reducer": { "params": [], "type": "avg" }, "type": "query" } ], "executionErrorState": "keep_state", "frequency": "60s", "handler": 1, "name": "failedPush alert", "noDataState": "ok", "notifications": [] }, "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 5, "w": 8, "x": 16, "y": 47 }, "hiddenSeries": false, "id": 51, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nacos_monitor{name='failedPush',env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [ { "colorMode": "critical", "fill": true, "line": true, "op": "gt", "value": 1, "visible": true } ], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "failed push alert", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values({job=\"nacosExporter\"},env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values({job=\"nacosExporter\"},env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": ".*:8848", "current": {}, "datasource": "Prometheus", "definition": "label_values({job=\"nacosExporter\",env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values({job=\"nacosExporter\",env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/14-postgresql-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 14, "uid": "000000039", "title": "PostgreSQL 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 34, "panels": [], "title": "General Counters, CPU, Memory and File Descriptor Stats", "type": "row" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "#299c46", "#7eb26d", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 0, "y": 1 }, "id": 36, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_static{release=\"$release\", env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "{{short_version}}", "refId": "A" } ], "thresholds": "", "title": "Version", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "name" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "description": "start time of the process", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "dateTimeFromNow", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 4, "y": 1 }, "id": 28, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "110%", "prefix": "", "prefixFontSize": "110%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_postmaster_start_time_seconds{env=~\"$env\",instance=\"$instance\"} * 1000", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Start Time", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "avg" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "decbytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 8, "y": 1 }, "height": "200px", "id": 10, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "SUM(pg_stat_database_tup_fetched{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 4 } ], "thresholds": "", "title": "Current fetch data", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "decbytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 12, "y": 1 }, "height": "200px", "id": 11, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "SUM(pg_stat_database_tup_inserted{release=\"$release\", datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 4 } ], "thresholds": "", "title": "Current insert data", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "decbytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 16, "y": 1 }, "height": "200px", "id": 12, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "SUM(pg_stat_database_tup_updated{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 4 } ], "thresholds": "", "title": "Current update data", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 2, "w": 4, "x": 20, "y": 1 }, "id": 38, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_max_connections{release=\"$release\", env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Max Connections", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "avg" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Average user and system CPU time spent in seconds.", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 3 }, "hiddenSeries": false, "id": 22, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "avg(rate(process_cpu_seconds_total{release=\"$release\", env=~\"$env\",instance=\"$instance\"}[5m]) * 1000)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "CPU Time", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Average CPU Usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "s", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Virtual and Resident memory size in bytes, averages over 5 min interval", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 3 }, "hiddenSeries": false, "id": 24, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "avg(rate(process_resident_memory_bytes{release=\"$release\", env=~\"$env\",instance=\"$instance\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Resident Mem", "refId": "A" }, { "expr": "avg(rate(process_virtual_memory_bytes{release=\"$release\", env=~\"$env\",instance=\"$instance\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Virtual Mem", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Average Memory Usage", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Number of open file descriptors", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 3 }, "hiddenSeries": false, "id": 26, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "process_open_fds{release=\"$release\", env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Open FD", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Open File Descriptors", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 }, "id": 32, "panels": [], "title": "Settings", "type": "row" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 0, "y": 11 }, "id": 40, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_shared_buffers_bytes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Shared Buffers", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 3, "y": 11 }, "id": 42, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_effective_cache_size_bytes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Effective Cache", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 6, "y": 11 }, "id": 44, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_maintenance_work_mem_bytes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Maintenance Work Mem", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 9, "y": 11 }, "id": 46, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_work_mem_bytes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Work Mem", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "decimals": 1, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 12, "y": 11 }, "id": 48, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_max_wal_size_bytes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Max WAL Size", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 3, "x": 15, "y": 11 }, "id": 50, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_random_page_cost{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Random Page Cost", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 2, "x": 18, "y": 11 }, "id": 52, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_seq_page_cost{env=~\"$env\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Seq Page Cost", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 2, "x": 20, "y": 11 }, "id": 54, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_max_worker_processes{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Max Worker Processes", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "avg" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a" ], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 3, "w": 2, "x": 22, "y": 11 }, "id": 56, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "pg_settings_max_parallel_workers{env=~\"$env\",instance=\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": "", "title": "Max Parallel Workers", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 14 }, "id": 30, "panels": [], "title": "Database Stats", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 15 }, "hiddenSeries": false, "id": 1, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 3, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_activity_count{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\", state=\"active\"} !=0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}, s: {{state}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Active sessions", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "none", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 15 }, "hiddenSeries": false, "id": 60, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(pg_stat_database_xact_commit{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{datname}} commits", "refId": "A" }, { "expr": "irate(pg_stat_database_xact_rollback{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{datname}} rollbacks", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Transactions", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 15 }, "hiddenSeries": false, "id": 8, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sideWidth": null, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_tup_updated{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Update data", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 22 }, "hiddenSeries": false, "id": 5, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_tup_fetched{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Fetch data (SELECT)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 22 }, "hiddenSeries": false, "id": 6, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_tup_inserted{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Insert data", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 22 }, "hiddenSeries": false, "id": 3, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": false, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_locks_count{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\", mode=~\"$mode\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}},{{mode}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Lock tables", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 29 }, "hiddenSeries": false, "id": 14, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "total", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_tup_returned{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Return data", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 29 }, "hiddenSeries": false, "id": 4, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": false, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_activity_count{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\", state=~\"idle|idle in transaction|idle in transaction (aborted)\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}, s: {{state}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Idle sessions", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 29 }, "hiddenSeries": false, "id": 7, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "sort": "current", "sortDesc": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_tup_deleted{datname=~\"$datname\", env=~\"$env\",instance=~\"$instance\"} != 0", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{datname}}", "refId": "A", "step": 2 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Delete data", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 36 }, "hiddenSeries": false, "id": 62, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "pg_stat_database_blks_hit{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"} / (pg_stat_database_blks_read{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"} + pg_stat_database_blks_hit{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ datname }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Cache Hit Rate", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 4, "format": "percentunit", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 36 }, "hiddenSeries": false, "id": 64, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(pg_stat_bgwriter_buffers_backend{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "buffers_backend", "refId": "A" }, { "expr": "irate(pg_stat_bgwriter_buffers_alloc{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "buffers_alloc", "refId": "B" }, { "expr": "irate(pg_stat_bgwriter_buffers_backend_fsync{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "backend_fsync", "refId": "C" }, { "expr": "irate(pg_stat_bgwriter_buffers_checkpoint{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "buffers_checkpoint", "refId": "D" }, { "expr": "irate(pg_stat_bgwriter_buffers_clean{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "buffers_clean", "refId": "E" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Buffers (bgwriter)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 36 }, "hiddenSeries": false, "id": 66, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(pg_stat_database_conflicts{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{datname}} conflicts", "refId": "B" }, { "expr": "irate(pg_stat_database_deadlocks{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{datname}} deadlocks", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Conflicts/Deadlocks", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Total amount of data written to temporary files by queries in this database. All temporary files are counted, regardless of why the temporary file was created, and regardless of the log_temp_files setting.", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 43 }, "hiddenSeries": false, "id": 68, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(pg_stat_database_temp_bytes{env=~\"$env\",instance=\"$instance\", datname=~\"$datname\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{datname}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Temp File (Bytes)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 16, "x": 8, "y": 43 }, "hiddenSeries": false, "id": 70, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(pg_stat_bgwriter_checkpoint_write_time{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "write_time - Total amount of time that has been spent in the portion of checkpoint processing where files are written to disk.", "refId": "B" }, { "expr": "irate(pg_stat_bgwriter_checkpoint_sync_time{env=~\"$env\",instance=\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "sync_time - Total amount of time that has been spent in the portion of checkpoint processing where files are synchronized to disk.", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Checkpoint Stats", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "Namespace", "multi": false, "name": "namespace", "options": [], "query": { "query": "query_result(pg_exporter_last_scrape_duration_seconds)", "refId": "Prometheus-namespace-Variable-Query" }, "refresh": 2, "regex": "/.*kubernetes_namespace=\"([^\"]+).*/", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "Release", "multi": false, "name": "release", "options": [], "query": { "query": "query_result(pg_exporter_last_scrape_duration_seconds{kubernetes_namespace=\"$namespace\"})", "refId": "Prometheus-release-Variable-Query" }, "refresh": 2, "regex": "/.*release=\"([^\"]+)/", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(pg_static,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(pg_static,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "query_result(pg_up{release=\"$release\",env=\"$env\"})", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Instance", "multi": false, "name": "instance", "options": [], "query": { "query": "query_result(pg_up{release=\"$release\",env=\"$env\"})", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "/.*instance=\"([^\"]+).*/", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "", "description": null, "error": null, "hide": 0, "includeAll": true, "label": "Database", "multi": true, "name": "datname", "options": [], "query": { "query": "label_values(datname)", "refId": "Prometheus-datname-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "", "description": null, "error": null, "hide": 2, "includeAll": true, "label": "Lock table", "multi": true, "name": "mode", "options": [], "query": { "query": "label_values({mode=~\"accessexclusivelock|accesssharelock|exclusivelock|rowexclusivelock|rowsharelock|sharelock|sharerowexclusivelock|shareupdateexclusivelock\"}, mode)", "refId": "Prometheus-mode-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/15-redis-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 15, "uid": "QuS4Sq0Mz", "title": "Redis 信息面板", "panels": [ { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "s", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 2, "x": 0, "y": 0 }, "id": 9, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "expr": "max(max_over_time(redis_uptime_in_seconds{env=\"$env\",instance=~\"$instance\"}[$__interval]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "metric": "", "refId": "A", "step": 1800 } ], "thresholds": "", "title": "Uptime", "type": "singlestat", "valueFontSize": "70%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 6, "x": 2, "y": 0 }, "hideTimeOverride": true, "id": 12, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "expr": "redis_connected_clients{env=\"$env\",instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "metric": "", "refId": "A", "step": 2 } ], "thresholds": "", "timeFrom": "1m", "timeShift": null, "title": "Clients", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 0 }, "hiddenSeries": false, "id": 2, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(redis_commands_processed_total{env=~\"$env\",instance=~\"$instance\"}[1m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "", "metric": "A", "refId": "A", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Commands Executed / sec", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 0 }, "hiddenSeries": false, "id": 1, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": true, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(redis_keyspace_hits_total{env=~\"$env\",instance=~\"$instance\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "hits", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "irate(redis_keyspace_misses_total{env=~\"$env\",instance=~\"$instance\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "misses", "metric": "", "refId": "B", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Hits / Misses per Sec", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "max": "#BF1B00" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "hiddenSeries": false, "id": 7, "isNew": true, "legend": { "avg": false, "current": false, "hideEmpty": false, "hideZero": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "redis_memory_used_bytes{env=~\"$env\",instance=~\"$instance\"} ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "used", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "redis_memory_max_bytes{env=~\"$env\",instance=~\"$instance\"} ", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "max", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Total Memory Usage", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 10, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(redis_net_input_bytes_total{env=~\"$env\",instance=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "input", "refId": "A", "step": 240 }, { "expr": "rate(redis_net_output_bytes_total{env=~\"$env\",instance=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "output", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Network I/O", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 7, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 5, "isNew": true, "legend": { "alignAsTable": true, "avg": false, "current": true, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum (redis_db_keys{env=~\"$env\",instance=~\"$instance\"}) by (db)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ db }} ", "refId": "A", "step": 240, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Total Items per DB", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "none", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 7, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 13, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "sum (redis_db_keys{env=~\"$env\",instance=~\"$instance\"}) - sum (redis_db_keys_expiring{env=~\"$env\",instance=~\"$instance\"}) ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "not expiring", "refId": "A", "step": 240, "target": "" }, { "expr": "sum (redis_db_keys_expiring{env=~\"$env\",instance=~\"$instance\"}) ", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "expiring", "metric": "", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Expiring vs Not-Expiring Keys", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "evicts": "#890F02", "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02", "reclaims": "#3F6833" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 8, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "reclaims", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(redis_expired_keys_total{env=~\"$env\",instance=~\"$instance\"}[5m])) by (instance)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "expired", "metric": "", "refId": "A", "step": 240, "target": "" }, { "expr": "sum(rate(redis_evicted_keys_total{env=~\"$env\",instance=~\"$instance\"}[5m])) by (instance)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "evicted", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Expired / Evicted", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 8, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 21 }, "hiddenSeries": false, "id": 14, "isNew": true, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "topk(5, irate(redis_commands_total{env=~\"$env\",instance=~\"$instance\"} [1m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ cmd }}", "metric": "redis_command_calls_total", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Command Calls / sec", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(redis_uptime_in_seconds, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(redis_uptime_in_seconds, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(redis_up{env=\"$env\"}, instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": null, "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(redis_up{env=\"$env\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/16-tengine-nginx-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 16, "uid": "9MOLXSbMz", "title": "Tengine (Nginx) 信息面板", "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 1, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "nginx_server_connections{env=~\"$env\",instance=~\"$instance\",status=~\"active|writing|reading|waiting\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{status}}", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_cache{env=~\"$env\",instance=~\"$instance\"}[5m])) by (status)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ status }}", "metric": "nginx_server_cache", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "hiddenSeries": false, "id": 3, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_requests{env=~\"$env\",instance=~\"$instance\", code!=\"total\"}[5m])) by (code)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ code }}", "metric": "nginx_server_requests", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Requests", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 2, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_bytes{env=~\"$env\",instance=~\"$instance\"}[5m])) by (direction)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ direction }}", "metric": "nginx_server_bytes", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Bytes", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "This one is providing aggregated error codes, but it's still possible to graph these per upstream.", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_upstream_requests{env=~\"$env\",instance=~\"$instance\", upstream=~\"^$upstream$\",code!=\"total\"}[5m])) by (code)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ code }}", "metric": "nginx_upstream_requests", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Requests", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 5, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_upstream_bytes{env=~\"$env\",instance=~\"$instance\", upstream=~\"^$upstream$\"}[5m])) by (direction)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ direction }}", "metric": "nginx_upstream_bytes", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Bytes", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 7, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nginx_upstream_responseMsec{env=~\"$env\",instance=~\"$instance\", upstream=~\"^$upstream$\"}) by (backend)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ backend }}", "metric": "nginx_upstream_response", "refId": "A", "step": 120 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Backend Response", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(nginx_server_bytes,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(nginx_server_bytes,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(nginx_server_bytes{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(nginx_server_bytes{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": ".*", "current": {}, "datasource": "Prometheus", "definition": "label_values(nginx_upstream_bytes{env=\"$env\",instance=\"$instance\"}, upstream)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": "upstream", "multi": false, "name": "upstream", "options": [], "query": { "query": "label_values(nginx_upstream_bytes{env=\"$env\",instance=\"$instance\"}, upstream)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/17-zookeeper-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 17, "uid": "SDE76m7Zzz", "title": "ZooKeeper 信息面板", "panels": [ { "cacheTimeout": null, "colorBackground": true, "colorValue": false, "colors": [ "#F2495C", "#F2495C", "#F2495C" ], "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "ms", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "id": 225, "interval": null, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "pluginVersion": "6.2.2", "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "zk_server_leader{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "thresholds": "", "timeFrom": null, "timeShift": null, "title": "leader", "type": "singlestat", "valueFontSize": "120%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "name" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 212, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_watch_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} watch_count", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "watch_count", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "max": 1024, "min": 0, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "index": 0, "value": null }, { "color": "red", "index": 1, "value": 800 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 8 }, "id": 122, "links": [], "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "last" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "zk_open_file_descriptor_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}}", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "open_file_descriptor", "type": "gauge" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "hiddenSeries": false, "id": 224, "interval": "", "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": false, "max": true, "min": true, "rightSide": false, "show": false, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_znode_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} znode_count", "refId": "A" }, { "expr": "zk_ephemerals_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} ephemerals", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "znode_count", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "hiddenSeries": false, "id": 4, "interval": "", "legend": { "alignAsTable": false, "avg": false, "current": true, "hideEmpty": false, "max": true, "min": true, "rightSide": false, "show": false, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(zk_znode_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}[5m])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} znode_count_rate", "refId": "A" }, { "expr": "rate(zk_ephemerals_count{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}[5m])", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} ephemerals_rate", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "znode_count_rate", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ops", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, "hiddenSeries": false, "id": 132, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_approximate_data_size{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}} approximate_data_size", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "approximate_data_size", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, "hiddenSeries": false, "id": 190, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_num_alive_connections{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}} num_alive_connections", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "num_alive_connections", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 32 }, "hiddenSeries": false, "id": 90, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_packets_received{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} packets_received", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "packets_received", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 32 }, "hiddenSeries": false, "id": 56, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_packets_sent{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} packets_sent", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "packets_sent", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 40 }, "hiddenSeries": false, "id": 92, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_outstanding_requests{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} outstanding_changes_queued", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "zk_outstanding_requests", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "$DS_PROMETHEUS", "decimals": null, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 40 }, "hiddenSeries": false, "id": 100, "interval": "", "legend": { "avg": true, "current": true, "max": true, "min": true, "show": false, "total": true, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "zk_min_latency{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} max_latency", "refId": "A" }, { "expr": "zk_max_latency{env=~\"$env\",instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} min_latency", "refId": "B" }, { "expr": "zk_avg_latency{instance=~\"$instance\",job=~\"$job\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}} avg_latency", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "max_min_avg_latency", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "current": { "selected": false, "text": "Prometheus", "value": "Prometheus" }, "description": null, "error": null, "hide": 2, "includeAll": false, "label": "Datasource", "multi": false, "name": "DS_PROMETHEUS", "options": [], "query": "prometheus", "queryValue": "", "refresh": 1, "regex": "", "skipUrlSync": false, "type": "datasource" }, { "allValue": null, "current": {}, "datasource": "$DS_PROMETHEUS", "definition": "label_values(zk_up,job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(zk_up,job)", "refId": "Prometheus-job-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(zk_up{job=~\"$job\"}, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(zk_up{job=~\"$job\"}, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "$DS_PROMETHEUS", "definition": "label_values(zk_up{job=~\"$job\",env=\"$env\"}, instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "Instance", "multi": true, "name": "instance", "options": [], "query": { "query": "label_values(zk_up{job=~\"$job\",env=\"$env\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/18-exporter-status.json ================================================ { "dashboard": { "id": 18, "uid": "dj43F8hT9", "title": "Exporter Status", "panels": [ { "cacheTimeout": null, "columns": [], "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fontSize": "100%", "gridPos": { "h": 18, "w": 24, "x": 0, "y": 0 }, "id": 2, "links": [], "pageSize": null, "pluginVersion": "6.5.2", "showHeader": true, "sort": { "col": 1, "desc": false }, "styles": [ { "alias": "Time", "align": "auto", "dateFormat": "YYYY-MM-DD HH:mm:ss", "pattern": "Time", "type": "hidden" }, { "alias": "Exporter", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Metric", "thresholds": [], "type": "number", "unit": "short" }, { "alias": "Status", "align": "auto", "colorMode": "cell", "colors": [ "#C4162A", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Value", "thresholds": [ "0", "1" ], "type": "string", "unit": "short", "valueMaps": [ { "text": "正常", "value": "1" }, { "text": "异常", "value": "0" } ] }, { "alias": "", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "decimals": 2, "pattern": "/.*/", "thresholds": [], "type": "number", "unit": "short" } ], "targets": [ { "expr": "exporter_status{env=~\"$env\",instance=~\"$ip\",app=~\"$app\"}", "instant": true, "interval": "", "legendFormat": "{{instance}}/{{app}}", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "Exporter Status", "transform": "timeseries_to_rows", "type": "table-old" } ], "templating": { "list": [ { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(exporter_status,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(exporter_status,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(exporter_status{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": "ip", "multi": true, "name": "ip", "options": [], "query": { "query": "label_values(exporter_status{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(exporter_status{env=\"$env\",instance=~\"$ip\"},app)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": null, "multi": true, "name": "app", "options": [], "query": { "query": "label_values(exporter_status{env=\"$env\",instance=~\"$ip\"},app)", "refId": "Prometheus-app-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/19-jvm-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 20, "panels": [ { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "decimals": 0, "mappings": [ { "from": "", "id": 1, "text": "正常", "to": "", "type": 1, "value": "1" }, { "from": "", "id": 2, "text": "异常", "to": "", "type": 1, "value": "0" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 0, "y": 0 }, "id": 2, "interval": null, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "repeat": null, "repeatDirection": "h", "targets": [ { "expr": "probe_success{env=~\"$env\",instance=~\"$ip\",app=~\"$app\",app!=\"node\"}", "format": "table", "instant": true, "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "服务状态", "transformations": [ { "id": "organize", "options": { "excludeByName": { "Time": true, "Value": false, "__name__": true, "app": false, "env": true, "instance": true, "job": true }, "indexByName": {}, "renameByName": { "Value": "服务状态", "app": "服务名称" } } }, { "id": "seriesToRows", "options": {} } ], "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 6, "y": 0 }, "id": 50, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_uptime_seconds{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "服务运行时间", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 11, "y": 0 }, "id": 60, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "system_cpu_usage{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"} * 100", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "cpu使用率", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 4, "x": 16, "y": 0 }, "id": 52, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_files_open_files{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "应用当前打开文件描述符数量", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 4, "x": 20, "y": 0 }, "id": 58, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "system_load_average_1m{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "系统一分钟负载占用情况", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "dateTimeAsLocal" }, "overrides": [] }, "gridPos": { "h": 7, "w": 6, "x": 0, "y": 7 }, "id": 48, "interval": null, "links": [], "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_start_time_seconds{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"} * 1000 ", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "服务启动时间", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 6, "y": 7 }, "id": 72, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "tomcat_sessions_active_current_sessions{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "Tomcat当前活跃session数量", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 11, "y": 7 }, "id": 54, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_files_max_files{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "应用可打开的最大文件描述符数量", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 4, "x": 16, "y": 7 }, "id": 20, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "jvm_threads_daemon_threads{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "JVM线程的当前数量", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 8, "y": 14 }, "id": 79, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "sum(jvm_memory_used_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\", area=\"heap\"})*100/sum(jvm_memory_max_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\", area=\"heap\"})", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "Heap 使用率", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 7, "w": 5, "x": 13, "y": 14 }, "id": 80, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "sum(jvm_memory_used_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\", area=\"nonheap\"})*100/sum(jvm_memory_max_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\", area=\"nonheap\"})", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "Non-Heap 使用率", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 7, "w": 4, "x": 20, "y": 7 }, "id": 56, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "system_cpu_count{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "java虚拟机可用的cpu数量", "type": "gauge" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_pause_seconds_count{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_pause_seconds_count", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 24, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 8, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_pause_seconds_max{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_pause_seconds_max", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 24, "x": 0, "y": 30 }, "hiddenSeries": false, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_pause_seconds_sum{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_pause_seconds_sum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 39 }, "hiddenSeries": false, "id": 12, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_classes_loaded_classes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_classes_loaded_classes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 39 }, "hiddenSeries": false, "id": 10, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_live_data_size_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_live_data_size_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 47 }, "hiddenSeries": false, "id": 40, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_used_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"nonheap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_used_bytes(nonheap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 47 }, "hiddenSeries": false, "id": 14, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_used_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"heap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_used_bytes(heap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 55 }, "hiddenSeries": false, "id": 42, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_max_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"nonheap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_max_bytes(nonheap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 55 }, "hiddenSeries": false, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_max_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"heap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_max_bytes(heap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 63 }, "hiddenSeries": false, "id": 44, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_committed_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"nonheap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_committed_bytes(nonheap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 63 }, "hiddenSeries": false, "id": 18, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_memory_committed_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",area=\"heap\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_memory_committed_bytes(heap)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 71 }, "hiddenSeries": false, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_classes_unloaded_classes_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_classes_unloaded_classes_total", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 71 }, "hiddenSeries": false, "id": 24, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_max_data_size_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_max_data_size_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 79 }, "hiddenSeries": false, "id": 28, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_threads_live_threads{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_threads_live_threads", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 79 }, "hiddenSeries": false, "id": 30, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_buffer_memory_used_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_buffer_memory_used_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 87 }, "hiddenSeries": false, "id": 34, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_buffer_count_buffers{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_buffer_count_buffers", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 87 }, "hiddenSeries": false, "id": 32, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_threads_peak_threads{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_threads_peak_threads", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 95 }, "hiddenSeries": false, "id": 64, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "tomcat_sessions_created_sessions_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Tomcat创建的session总数", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 95 }, "hiddenSeries": false, "id": 36, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_memory_promoted_bytes_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_memory_promoted_bytes_total", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 103 }, "hiddenSeries": false, "id": 68, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "tomcat_sessions_expired_sessions_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Tomcat过期的session总数", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 103 }, "hiddenSeries": false, "id": 38, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_buffer_total_capacity_bytes{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_buffer_total_capacity_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 111 }, "hiddenSeries": false, "id": 70, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "tomcat_sessions_alive_max_seconds{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Tomcat存活session最大数", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 111 }, "hiddenSeries": false, "id": 62, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "tomcat_sessions_active_max_sessions{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Tomcat最多活跃session数持续时间", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 119 }, "hiddenSeries": false, "id": 46, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "jvm_gc_memory_allocated_bytes_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "jvm_gc_memory_allocated_bytes_total", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 119 }, "hiddenSeries": false, "id": 66, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "tomcat_sessions_rejected_sessions_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "超过session最大配置后,拒绝的session个数", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 127 }, "hiddenSeries": false, "id": 78, "legend": { "alignAsTable": false, "avg": false, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": true, "show": true, "sideWidth": 750, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "log4j2_events_total{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "日志级别事件个数", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:242", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:243", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": true, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": {}, "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 135 }, "hiddenSeries": false, "id": 74, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_server_requests_seconds_count{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "http请求接口次数统计", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 150 }, "hiddenSeries": false, "id": 82, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_server_requests_seconds_count{env=~\"$env\",instance=~\"$ip\",job=~\"$job\",status=~\"5..\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "http请求接口--5xx errors 次数统计", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": {}, "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 24, "x": 0, "y": 143 }, "hiddenSeries": false, "id": 76, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_server_requests_seconds_sum{env=~\"$env\",instance=~\"$ip\",job=~\"$job\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "http请求接口耗时统计", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(probe_success,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(probe_success,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(probe_success{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "ip", "multi": false, "name": "ip", "options": [], "query": { "query": "label_values(probe_success{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(probe_success{env=\"$env\",instance=\"$ip\"},app)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "app", "multi": false, "name": "app", "options": [], "query": { "query": "label_values(probe_success{env=\"$env\",instance=\"$ip\"},app)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(probe_success{env=\"$env\",instance=~\"$ip\",app=\"$app\"},job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [ ], "query": { "query": "label_values(probe_success{env=\"$env\",instance=~\"$ip\",app=\"$app\"},job)", "refId": "StandardVariableQuery" }, "refresh": 0, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "title": "JavaSpringBoot 信息面板", "uid": "VOtGCaInz", "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/2-fu-wu-zhuang-tai-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 2, "uid": "9CSxoPAGz", "title": "服务状态 信息面板", "panels": [ { "cacheTimeout": null, "columns": [], "datasource": "Prometheus", "fontSize": "100%", "gridPos": { "h": 18, "w": 24, "x": 0, "y": 0 }, "id": 2, "links": [], "pageSize": null, "pluginVersion": "6.5.2", "showHeader": true, "sort": { "col": 1, "desc": false }, "styles": [ { "alias": "Time", "align": "auto", "dateFormat": "YYYY-MM-DD HH:mm:ss", "pattern": "Time", "type": "hidden" }, { "alias": "Service", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Metric", "thresholds": [], "type": "number", "unit": "short" }, { "alias": "Status", "align": "auto", "colorMode": "cell", "colors": [ "#C4162A", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "dateFormat": "YYYY-MM-DD HH:mm:ss", "decimals": 2, "mappingType": 1, "pattern": "Value", "thresholds": [ "0", "1" ], "type": "string", "unit": "short", "valueMaps": [ { "text": "正常", "value": "1" }, { "text": "异常", "value": "0" } ] }, { "alias": "", "align": "auto", "colorMode": null, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "decimals": 2, "pattern": "/.*/", "thresholds": [], "type": "number", "unit": "short" } ], "targets": [ { "expr": "probe_success{env=~\"$env\",instance=~\"$ip\",app=~\"$app\",app!~\"node\"}", "instant": true, "interval": "", "legendFormat": "{{instance}}/{{app}}", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "Service Status", "transform": "timeseries_to_rows", "type": "table-old" } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(probe_success,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(probe_success,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(probe_success{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": "ip", "multi": true, "name": "ip", "options": [], "query": { "query": "label_values(probe_success{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(probe_success{env=\"$env\",instance=~\"$ip\"},app)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": null, "multi": true, "name": "app", "options": [], "query": { "query": "label_values(probe_success{env=\"$env\",instance=~\"$ip\"},app)", "refId": "Prometheus-app-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/20-clickhousecluster-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 20, "uid": "VqK1fIBMkk", "title": "ClickhouseCluster 信息面板", "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 1, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "rate(clickhouse_query{cluster=~\"$cluster\"}[1m])", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Query", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 2, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/merge.*/", "bars": false, "lines": true }, { "alias": "/rate.*/", "bars": true, "lines": false, "yaxis": 2, "zindex": -1 } ], "spaceLength": 10, "stack": false, "steppedLine": true, "targets": [ { "expr": "clickhouse_merge{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "merge {{instance}}", "refId": "A", "step": 4 }, { "expr": "rate(clickhouse_merged_rows_total{cluster=~\"$cluster\"}[1m])", "interval": "", "intervalFactor": 2, "legendFormat": "rate {{instance}}", "refId": "B", "step": 4 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Merge", "tooltip": { "msResolution": false, "shared": true, "sort": 2, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "id": 6, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(clickhouse_readonly_replica{cluster=~\"$cluster\"})", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 60 } ], "thresholds": "1,1", "title": "ReadOnly replica", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 3, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_replicated_checks{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 20 }, { "expr": "clickhouse_replicated_fetch{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "B", "step": 20 }, { "expr": "clickhouse_replicated_send{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Replication", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 4, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_read{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 }, { "expr": "clickhouse_write{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "B", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Read/Write", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 9, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_background_pool_task{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}}", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Pool Tasks", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 5, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_tcp_connection{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "tcp {{instance}}", "refId": "A", "step": 10 }, { "expr": "clickhouse_http_connection{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "http {{instance}}", "refId": "B", "step": 10 }, { "expr": "clickhouse_interserver_connection{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "interserver {{instance}}", "refId": "C", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "unit": "decbytes" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 21 }, "hiddenSeries": false, "id": 10, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_memory_tracking{cluster=~\"$cluster\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Memory", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(clickhouse_query,cluster)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "集群", "multi": false, "name": "cluster", "options": [], "query": { "query": "label_values(clickhouse_query,cluster)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/22-mysqlcluster-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 22, "uid": "MQWgroiiz1", "title": "MysqlCluster 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 382, "panels": [], "repeat": null, "title": "", "type": "row" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 1, "description": "**MySQL Uptime**\n\nThe amount of time since the last restart of the MySQL server process.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "s", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 1 }, "height": "125px", "id": 12, "interval": "", "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "s", "postfixFontSize": "80%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_uptime{cluster=\"$cluster\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "{{ instance_name }}", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "300,3600", "title": "MySQL Uptime", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 2, "description": "**Current QPS**\n\nBased on the queries reported by MySQL's ``SHOW STATUS`` command, it is the number of statements executed by the server within the last second. This variable includes statements executed within stored programs, unlike the Questions variable. It does not count \n``COM_PING`` or ``COM_STATISTICS`` commands.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "short", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 1 }, "height": "125px", "id": 13, "interval": "", "links": [ { "targetBlank": true, "title": "MySQL Server Status Variables", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Queries" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": true }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_queries{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_queries{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": "35,75", "title": "Current QPS", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": false, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "datasource": "Prometheus", "decimals": 0, "description": "**InnoDB Buffer Pool Size**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "bytes", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 1 }, "height": "125px", "id": 51, "interval": "", "links": [ { "targetBlank": true, "title": "Tuning the InnoDB Buffer Pool Size", "url": "https://www.percona.com/blog/2015/06/02/80-ram-tune-innodb_buffer_pool_size/" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_innodb_buffer_pool_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "{{ instance_name }}", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "90,95", "title": "InnoDB Buffer Pool Size", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)" ], "datasource": "Prometheus", "decimals": 0, "description": "**InnoDB Buffer Pool Size % of Total RAM**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "percent", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 1 }, "height": "125px", "id": 52, "interval": "", "links": [ { "targetBlank": true, "title": "Tuning the InnoDB Buffer Pool Size", "url": "https://www.percona.com/blog/2015/06/02/80-ram-tune-innodb_buffer_pool_size/" } ], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "80%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "repeat": null, "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "calculatedInterval": "10m", "datasourceErrors": {}, "errors": {}, "expr": "(mysql_global_variables_innodb_buffer_pool_size{cluster=\"$cluster\"} * 100) / on (instance) node_memory_MemTotal{cluster=\"$cluster\"}", "format": "time_series", "interval": "5m", "intervalFactor": 1, "legendFormat": "{{ instance_name }}", "metric": "", "refId": "A", "step": 300 } ], "thresholds": "40,80", "title": "Buffer Pool Size of Total RAM", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [], "valueName": "current" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 383, "panels": [], "repeat": null, "title": "Connections", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "description": "**Max Connections** \n\nMax Connections is the maximum permitted number of simultaneous client connections. By default, this is 151. Increasing this value increases the number of file descriptors that mysqld requires. If the required number of descriptors are not available, the server reduces the value of Max Connections.\n\nmysqld actually permits Max Connections + 1 clients to connect. The extra connection is reserved for use by accounts that have the SUPER privilege, such as root.\n\nMax Used Connections is the maximum number of connections that have been in use simultaneously since the server started.\n\nConnections is the number of connection attempts (successful or not) to the MySQL server.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 6 }, "height": "250px", "hiddenSeries": false, "id": 92, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "targetBlank": true, "title": "MySQL Server System Variables", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_connections" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Max Connections", "fill": 0 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max(max_over_time(mysql_global_status_threads_connected{cluster=\"$cluster\"}[5m]) or mysql_global_status_threads_connected{cluster=\"$cluster\"} )", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Connections {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_max_used_connections{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Max Used Connections {{ instance_name }}", "metric": "", "refId": "C", "step": 20, "target": "" }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_max_connections{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Max Connections {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Active Threads**\n\nThreads Connected is the number of open connections, while Threads Running is the number of threads not sleeping.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 6 }, "hiddenSeries": false, "id": 10, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": false, "show": true, "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Peak Threads Running", "color": "#E24D42", "lines": false, "pointradius": 1, "points": true }, { "alias": "Peak Threads Connected", "color": "#1F78C1" }, { "alias": "Avg Threads Running", "color": "#EAB839" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max_over_time(mysql_global_status_threads_connected{cluster=\"$cluster\"}[5m]) or\nmax_over_time(mysql_global_status_threads_connected{cluster=\"$cluster\"}[5m])", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Peak Threads Connected {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "max_over_time(mysql_global_status_threads_running{cluster=\"$cluster\"}[5m]) or\nmax_over_time(mysql_global_status_threads_running{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Peak Threads Running {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "expr": "avg_over_time(mysql_global_status_threads_running{cluster=\"$cluster\"}[5m]) or \navg_over_time(mysql_global_status_threads_running{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Avg Threads Running {{ instance_name }}", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Client Thread Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [ "total" ] }, "yaxes": [ { "format": "short", "label": "Threads", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 13 }, "id": 384, "panels": [], "repeat": null, "title": "Table Locks", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "description": "**MySQL Questions**\n\nThe number of statements executed by the server. This includes only statements sent to the server by clients and not statements executed within stored programs, unlike the Queries used in the QPS calculation. \n\nThis variable does not count the following commands:\n* ``COM_PING``\n* ``COM_STATISTICS``\n* ``COM_STMT_PREPARE``\n* ``COM_STMT_CLOSE``\n* ``COM_STMT_RESET``", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 53, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "targetBlank": true, "title": "MySQL Queries and Questions", "url": "https://www.percona.com/blog/2014/05/29/how-mysql-queries-and-questions-are-measured/" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_questions{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_questions{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Questions {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Questions", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Thread Cache**\n\nThe thread_cache_size variable sets how many threads the server should cache to reuse. When a client disconnects, the client's threads are put in the cache if the cache is not full. It is autosized in MySQL 5.6.8 and above (capped to 100). Requests for threads are satisfied by reusing threads taken from the cache if possible, and only when the cache is empty is a new thread created.\n\n* *Threads_created*: The number of threads created to handle connections.\n* *Threads_cached*: The number of threads in the thread cache.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 11, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Tuning information", "url": "https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_thread_cache_size" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Threads Created", "fill": 0 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_thread_cache_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Thread Cache Size {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_threads_cached{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Threads Cached {{ instance_name }}", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_threads_created{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_threads_created{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Threads Created {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Thread Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 21 }, "id": 385, "panels": [], "repeat": null, "title": "Temporary Objects", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 22 }, "hiddenSeries": false, "id": 22, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_tables{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_created_tmp_tables{cluster=\"$cluster\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Tables {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_disk_tables{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_created_tmp_disk_tables{cluster=\"$cluster\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Disk Tables {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_created_tmp_files{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_created_tmp_files{cluster=\"$cluster\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Created Tmp Files {{ instance_name }}", "metric": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Temporary Objects", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Select Types**\n\nAs with most relational databases, selecting based on indexes is more efficient than scanning an entire table's data. Here we see the counters for selects not done with indexes.\n\n* ***Select Scan*** is how many queries caused full table scans, in which all the data in the table had to be read and either discarded or returned.\n* ***Select Range*** is how many queries used a range scan, which means MySQL scanned all rows in a given range.\n* ***Select Full Join*** is the number of joins that are not joined on an index, this is usually a huge performance hit.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 22 }, "height": "250px", "hiddenSeries": false, "id": 311, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_full_join{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_select_full_join{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Full Join {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_full_range_join{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_select_full_range_join{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Full Range Join {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_range{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_select_range{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Range {{ instance_name }}", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_range_check{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_select_range_check{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Range Check {{ instance_name }}", "metric": "", "refId": "D", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_select_scan{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_select_scan{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Select Scan {{ instance_name }}", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Select Types", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 29 }, "id": 386, "panels": [], "repeat": null, "title": "Sorts", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Sorts**\n\nDue to a query's structure, order, or other requirements, MySQL sorts the rows before returning them. For example, if a table is ordered 1 to 10 but you want the results reversed, MySQL then has to sort the rows to return 10 to 1.\n\nThis graph also shows when sorts had to scan a whole table or a given range of a table in order to return the results and which could not have been sorted via an index.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 30 }, "hiddenSeries": false, "id": 30, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_rows{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_sort_rows{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Rows {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_range{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_sort_range{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Range {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_merge_passes{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_sort_merge_passes{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Merge Passes {{ instance_name }}", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_sort_scan{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_sort_scan{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Sort Scan {{ instance_name }}", "metric": "", "refId": "D", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Sorts", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Slow Queries**\n\nSlow queries are defined as queries being slower than the long_query_time setting. For example, if you have long_query_time set to 3, all queries that take longer than 3 seconds to complete will show on this graph.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 30 }, "hiddenSeries": false, "id": 48, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_slow_queries{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_slow_queries{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Slow Queries {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Slow Queries", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 37 }, "id": 387, "panels": [], "repeat": null, "title": "Aborted", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Aborted Connections**\n\nWhen a given host connects to MySQL and the connection is interrupted in the middle (for example due to bad credentials), MySQL keeps that info in a system table (since 5.6 this table is exposed in performance_schema).\n\nIf the amount of failed requests without a successful connection reaches the value of max_connect_errors, mysqld assumes that something is wrong and blocks the host from further connection.\n\nTo allow connections from that host again, you need to issue the ``FLUSH HOSTS`` statement.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 38 }, "hiddenSeries": false, "id": 47, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_aborted_connects{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_aborted_connects{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Aborted Connects (attempts) {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_aborted_clients{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_aborted_clients{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Aborted Clients (timeout) {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Aborted Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Table Locks**\n\nMySQL takes a number of different locks for varying reasons. In this graph we see how many Table level locks MySQL has requested from the storage engine. In the case of InnoDB, many times the locks could actually be row locks as it only takes table level locks in a few specific cases.\n\nIt is most useful to compare Locks Immediate and Locks Waited. If Locks waited is rising, it means you have lock contention. Otherwise, Locks Immediate rising and falling is normal activity.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 38 }, "hiddenSeries": false, "id": 32, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_table_locks_immediate{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_locks_immediate{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Locks Immediate {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_table_locks_waited{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_locks_waited{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Locks Waited {{ instance_name }}", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Locks", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 45 }, "id": 388, "panels": [], "repeat": null, "title": "Network", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Network Traffic**\n\nHere we can see how much network traffic is generated by MySQL. Outbound is network traffic sent from MySQL and Inbound is network traffic MySQL has received.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 46 }, "hiddenSeries": false, "id": 9, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_bytes_received{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_bytes_received{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inbound {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_bytes_sent{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_bytes_sent{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Outbound {{ instance_name }}", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Network Traffic", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Network Usage Hourly**\n\nHere we can see how much network traffic is generated by MySQL per hour. You can use the bar graph to compare data sent by MySQL and data received by MySQL.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 46 }, "height": "250px", "hiddenSeries": false, "id": 381, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "increase(mysql_global_status_bytes_received{cluster=\"$cluster\"}[1h])", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Received {{ instance_name }}", "metric": "", "refId": "A", "step": 3600 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "increase(mysql_global_status_bytes_sent{cluster=\"$cluster\"}[1h])", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Sent {{ instance_name }}", "metric": "", "refId": "B", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "MySQL Network Usage Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 53 }, "id": 389, "panels": [], "repeat": null, "title": "Memory", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 0, "description": "***System Memory***: Total Memory for the system.\\\n***InnoDB Buffer Pool Data***: InnoDB maintains a storage area called the buffer pool for caching data and indexes in memory.\\\n***TokuDB Cache Size***: Similar in function to the InnoDB Buffer Pool, TokuDB will allocate 50% of the installed RAM for its own cache.\\\n***Key Buffer Size***: Index blocks for MYISAM tables are buffered and are shared by all threads. key_buffer_size is the size of the buffer used for index blocks.\\\n***Adaptive Hash Index Size***: When InnoDB notices that some index values are being accessed very frequently, it builds a hash index for them in memory on top of B-Tree indexes.\\\n ***Query Cache Size***: The query cache stores the text of a SELECT statement together with the corresponding result that was sent to the client. The query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time.\\\n***InnoDB Dictionary Size***: The data dictionary is InnoDB ‘s internal catalog of tables. InnoDB stores the data dictionary on disk, and loads entries into memory while the server is running.\\\n***InnoDB Log Buffer Size***: The MySQL InnoDB log buffer allows transactions to run without having to write the log to disk before the transactions commit.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 54 }, "hiddenSeries": false, "id": 50, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Detailed descriptions about metrics", "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "System Memory", "fill": 0, "stack": false } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "node_memory_MemTotal{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "System Memory {{ instance_name }}", "refId": "G", "step": 4 }, { "expr": "mysql_global_status_innodb_page_size{cluster=\"$cluster\"} * on (instance) mysql_global_status_buffer_pool_pages{cluster=\"$cluster\",state=\"data\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Buffer Pool Data {{ instance_name }}", "refId": "A", "step": 20 }, { "expr": "mysql_global_variables_innodb_log_buffer_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Log Buffer Size {{ instance_name }}", "refId": "D", "step": 20 }, { "expr": "mysql_global_variables_innodb_additional_mem_pool_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "InnoDB Additional Memory Pool Size {{ instance_name }}", "refId": "H", "step": 40 }, { "expr": "mysql_global_status_innodb_mem_dictionary{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Dictionary Size {{ instance_name }}", "refId": "F", "step": 20 }, { "expr": "mysql_global_variables_key_buffer_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Key Buffer Size {{ instance_name }}", "refId": "B", "step": 20 }, { "expr": "mysql_global_variables_query_cache_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Query Cache Size {{ instance_name }}", "refId": "C", "step": 20 }, { "expr": "mysql_global_status_innodb_mem_adaptive_hash{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Adaptive Hash Index Size {{ instance_name }}", "refId": "E", "step": 20 }, { "expr": "mysql_global_variables_tokudb_cache_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "TokuDB Cache Size {{ instance_name }}", "refId": "I", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Internal Memory Overview", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 61 }, "id": 390, "panels": [], "repeat": null, "title": "Command, Handlers, Processes", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Top Command Counters**\n\nThe Com_{{xxx}} statement counter variables indicate the number of times each xxx statement has been executed. There is one status variable for each type of statement. For example, Com_delete and Com_update count [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements, respectively. Com_delete_multi and Com_update_multi are similar but apply to [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements that use multiple-table syntax.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 62 }, "hiddenSeries": false, "id": 14, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideEmpty": false, "hideZero": false, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (Com_xxx)", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Com_xxx" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, rate(mysql_global_status_commands_total{cluster=\"$cluster\"}[5m])>0) or topk(5, irate(mysql_global_status_commands_total{cluster=\"$cluster\"}[5m])>0)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "Com_{{ command }} {{ instance_name }}", "metric": "", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Top Command Counters", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**Top Command Counters Hourly**\n\nThe Com_{{xxx}} statement counter variables indicate the number of times each xxx statement has been executed. There is one status variable for each type of statement. For example, Com_delete and Com_update count [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements, respectively. Com_delete_multi and Com_update_multi are similar but apply to [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements that use multiple-table syntax.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 69 }, "hiddenSeries": false, "id": 39, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [ { "title": "Server Status Variables (Com_xxx)", "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Com_xxx" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, increase(mysql_global_status_commands_total{cluster=\"$cluster\"}[1h])>0)", "format": "time_series", "interval": "1h", "intervalFactor": 1, "legendFormat": "Com_{{ command }} {{ instance_name }}", "metric": "", "refId": "A", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "Top Command Counters Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Handlers**\n\nHandler statistics are internal statistics on how MySQL is selecting, updating, inserting, and modifying rows, tables, and indexes.\n\nThis is in fact the layer between the Storage Engine and MySQL.\n\n* `read_rnd_next` is incremented when the server performs a full table scan and this is a counter you don't really want to see with a high value.\n* `read_key` is incremented when a read is done with an index.\n* `read_next` is incremented when the storage engine is asked to 'read the next index entry'. A high value means a lot of index scans are being done.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 76 }, "hiddenSeries": false, "id": 8, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_handlers_total{cluster=\"$cluster\", handler!~\"commit|rollback|savepoint.*|prepare\"}[5m]) or irate(mysql_global_status_handlers_total{cluster=\"$cluster\", handler!~\"commit|rollback|savepoint.*|prepare\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{ handler }} {{ instance_name }}", "metric": "", "refId": "J", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Handlers", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 83 }, "hiddenSeries": false, "id": 28, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": true, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_handlers_total{cluster=\"$cluster\", handler=~\"commit|rollback|savepoint.*|prepare\"}[5m]) or irate(mysql_global_status_handlers_total{cluster=\"$cluster\", handler=~\"commit|rollback|savepoint.*|prepare\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "{{ handler }} {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Transaction Handlers", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 0, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 90 }, "hiddenSeries": false, "id": 40, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_info_schema_threads{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "{{ state }} {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Process States", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 97 }, "hiddenSeries": false, "id": 49, "legend": { "alignAsTable": true, "avg": true, "current": false, "hideZero": true, "max": true, "min": false, "rightSide": true, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "topk(5, avg_over_time(mysql_info_schema_threads{cluster=\"$cluster\"}[1h]))", "interval": "1h", "intervalFactor": 1, "legendFormat": "{{ state }} {{ instance_name }}", "metric": "", "refId": "A", "step": 3600 } ], "thresholds": [], "timeFrom": "24h", "timeRegions": [], "timeShift": null, "title": "Top Process States Hourly", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 104 }, "id": 391, "panels": [], "repeat": null, "title": "Query Cache", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Query Cache Memory**\n\nThe query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time. This serialization is true not only for SELECTs, but also for INSERT/UPDATE/DELETE.\n\nThis also means that the larger the `query_cache_size` is set to, the slower those operations become. In concurrent environments, the MySQL Query Cache quickly becomes a contention point, decreasing performance. MariaDB and AWS Aurora have done work to try and eliminate the query cache contention in their flavors of MySQL, while MySQL 8.0 has eliminated the query cache feature.\n\nThe recommended settings for most environments is to set:\n ``query_cache_type=0``\n ``query_cache_size=0``\n\nNote that while you can dynamically change these values, to completely remove the contention point you have to restart the database.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 105 }, "hiddenSeries": false, "id": 46, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_qcache_free_memory{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Free Memory {{ instance_name }}", "metric": "", "refId": "F", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_query_cache_size{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Query Cache Size {{ instance_name }}", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Query Cache Memory", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Query Cache Activity**\n\nThe query cache has huge scalability problems in that only one thread can do an operation in the query cache at the same time. This serialization is true not only for SELECTs, but also for INSERT/UPDATE/DELETE.\n\nThis also means that the larger the `query_cache_size` is set to, the slower those operations become. In concurrent environments, the MySQL Query Cache quickly becomes a contention point, decreasing performance. MariaDB and AWS Aurora have done work to try and eliminate the query cache contention in their flavors of MySQL, while MySQL 8.0 has eliminated the query cache feature.\n\nThe recommended settings for most environments is to set:\n``query_cache_type=0``\n``query_cache_size=0``\n\nNote that while you can dynamically change these values, to completely remove the contention point you have to restart the database.", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 105 }, "height": "", "hiddenSeries": false, "id": 45, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_hits{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_qcache_hits{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Hits {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_inserts{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_qcache_inserts{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inserts {{ instance_name }}", "metric": "", "refId": "C", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_not_cached{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_qcache_not_cached{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Not Cached {{ instance_name }}", "metric": "", "refId": "D", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_qcache_lowmem_prunes{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_qcache_lowmem_prunes{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Prunes {{ instance_name }}", "metric": "", "refId": "F", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_qcache_queries_in_cache{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Queries in Cache {{ instance_name }}", "metric": "", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Query Cache Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 112 }, "id": 392, "panels": [], "repeat": null, "title": "Files and Tables", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 113 }, "hiddenSeries": false, "id": 43, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_opened_files{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_opened_files{cluster=\"$cluster\"}[5m])", "interval": "", "intervalFactor": 1, "legendFormat": "Openings {{ instance_name }}", "metric": "", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL File Openings", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 113 }, "hiddenSeries": false, "id": 41, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_files{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Open Files {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_open_files_limit{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Open Files Limit {{ instance_name }}", "metric": "", "refId": "D", "step": 20 }, { "expr": "mysql_global_status_innodb_num_open_files{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "InnoDB Open Files {{ instance_name }}", "refId": "B", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Open Files", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 120 }, "id": 393, "panels": [], "repeat": null, "title": "Table Openings", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Table Open Cache Status**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 121 }, "hiddenSeries": false, "id": 44, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Table Open Cache Hit Ratio", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "rate(mysql_global_status_opened_tables{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_opened_tables{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Openings {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Hits {{ instance_name }}", "refId": "B", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_misses{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_misses{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Misses {{ instance_name }}", "refId": "C", "step": 20 }, { "expr": "rate(mysql_global_status_table_open_cache_overflows{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_overflows{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Misses due to Overflows {{ instance_name }}", "refId": "D", "step": 20 }, { "expr": "(rate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m]))/((rate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_hits{cluster=\"$cluster\"}[5m]))+(rate(mysql_global_status_table_open_cache_misses{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_table_open_cache_misses{cluster=\"$cluster\"}[5m])))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Open Cache Hit Ratio {{ instance_name }}", "refId": "E", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Open Cache Status", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "percentunit", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Open Tables**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 121 }, "hiddenSeries": false, "id": 42, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_tables{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Open Tables {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_table_open_cache{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Open Cache {{ instance_name }}", "metric": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Open Tables", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 128 }, "id": 394, "panels": [], "repeat": null, "title": "MySQL Table Definition Cache", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "description": "**MySQL Table Definition Cache**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 129 }, "hiddenSeries": false, "id": 54, "legend": { "alignAsTable": true, "avg": true, "current": false, "max": true, "min": true, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [ { "title": "Server Status Variables (table_open_cache)", "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Opened Table Definitions", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_status_open_table_definitions{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Open Table Definitions {{ instance_name }}", "metric": "", "refId": "B", "step": 20 }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "mysql_global_variables_table_definition_cache{cluster=\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Table Definitions Cache Size {{ instance_name }}", "metric": "", "refId": "C", "step": 20 }, { "expr": "rate(mysql_global_status_opened_table_definitions{cluster=\"$cluster\"}[5m]) or irate(mysql_global_status_opened_table_definitions{cluster=\"$cluster\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Opened Table Definitions {{ instance_name }}", "refId": "A", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "MySQL Table Definition Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 136 }, "id": 395, "panels": [], "repeat": null, "title": "System Charts", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 137 }, "hiddenSeries": false, "id": 31, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pgpgin{cluster=\"$cluster\"}[5m]) * 1024 or irate(node_vmstat_pgpgin{cluster=\"$cluster\"}[5m]) * 1024", "interval": "", "intervalFactor": 1, "legendFormat": "Page In {{ instance_name }}", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pgpgout{cluster=\"$cluster\"}[5m]) * 1024 or irate(node_vmstat_pgpgout{cluster=\"$cluster\"}[5m]) * 1024", "interval": "", "intervalFactor": 1, "legendFormat": "Page Out {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "I/O Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 137 }, "height": "250px", "hiddenSeries": false, "id": 37, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_MemTotal{cluster=\"$cluster\"} - (node_memory_MemFree{cluster=\"$cluster\"} + node_memory_Buffers{cluster=\"$cluster\"} + node_memory_Cached{cluster=\"$cluster\"})", "interval": "", "intervalFactor": 1, "legendFormat": "Used {{ instance_name }}", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_MemFree{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Free {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_Buffers{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Buffers {{ instance_name }}", "metric": "", "refId": "D", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "node_memory_Cached{cluster=\"$cluster\"}", "interval": "", "intervalFactor": 1, "legendFormat": "Cached {{ instance_name }}", "metric": "", "refId": "E", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Memory Distribution", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": { "Load 1m": "#58140C", "Max Core Utilization": "#bf1b00", "iowait": "#e24d42", "nice": "#1f78c1", "softirq": "#806eb7", "system": "#eab839", "user": "#508642" }, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 6, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 137 }, "height": "", "hiddenSeries": false, "id": 2, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Max Core Utilization", "lines": false, "pointradius": 1, "points": true, "stack": false }, { "alias": "Load 1m", "color": "#58140C", "fill": 2, "stack": false, "yaxis": 2 } ], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "clamp_max(((avg by (mode) ( (clamp_max(rate(node_cpu{cluster=\"$cluster\",mode!=\"idle\"}[5m]),1)) or (clamp_max(irate(node_cpu{cluster=\"$cluster\",mode!=\"idle\"}[5m]),1)) ))*100 or (avg_over_time(node_cpu_average{env=\"$env\",instance=~\"$host\", mode!=\"total\", mode!=\"idle\"}[5m]) or avg_over_time(node_cpu_average{env=\"$env\",instance=~\"$host\", mode!=\"total\", mode!=\"idle\"}[5m]))),100)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ mode }} {{ instance_name }}", "metric": "", "refId": "A", "step": 20 }, { "expr": "clamp_max(max by () (sum by (cpu) ( (clamp_max(rate(node_cpu{cluster=\"$cluster\",mode!=\"idle\",mode!=\"iowait\"}[5m]),1)) or (clamp_max(irate(node_cpu{cluster=\"$cluster\",mode!=\"idle\",mode!=\"iowait\"}[5m]),1)) ))*100,100)", "format": "time_series", "hide": true, "interval": "", "intervalFactor": 1, "legendFormat": "Max Core Utilization {{ instance_name }}", "refId": "B", "step": 20 }, { "expr": "node_load1{cluster=\"$cluster\"}", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 2, "legendFormat": "Load 1m {{ instance_name }}", "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "CPU Usage / Load", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": 1, "format": "percent", "label": "", "logBase": 1, "max": 100, "min": 0, "show": true }, { "format": "none", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": 2, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 144 }, "height": "250px", "hiddenSeries": false, "id": 36, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": true }, "lines": false, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 1, "points": true, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "sum((rate(node_disk_read_time_ms{device!~\"dm-.+\", cluster=\"$cluster\"}[5m]) / rate(node_disk_reads_completed{device!~\"dm-.+\", cluster=\"$cluster\"}[5m])) or (irate(node_disk_read_time_ms{device!~\"dm-.+\", cluster=\"$cluster\"}[5m]) / irate(node_disk_reads_completed{device!~\"dm-.+\", cluster=\"$cluster\"}[5m]))\nor avg_over_time(aws_rds_read_latency_average{cluster=\"$cluster\"}[5m]) or avg_over_time(aws_rds_read_latency_average{cluster=\"$cluster\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Read {{ instance_name }}", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2m", "datasourceErrors": {}, "errors": {}, "expr": "sum((rate(node_disk_write_time_ms{device!~\"dm-.+\", cluster=\"$cluster\"}[5m]) / rate(node_disk_writes_completed{device!~\"dm-.+\", cluster=\"$cluster\"}[5m])) or (irate(node_disk_write_time_ms{device!~\"dm-.+\", cluster=\"$cluster\"}[5m]) / irate(node_disk_writes_completed{device!~\"dm-.+\", cluster=\"$cluster\"}[5m])) or \navg_over_time(aws_rds_write_latency_average{cluster=\"$cluster\"}[5m]) or avg_over_time(aws_rds_write_latency_average{cluster=\"$cluster\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Write {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Disk Latency", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "ms", "label": "", "logBase": 2, "max": null, "min": null, "show": true }, { "format": "ms", "label": "", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 144 }, "height": "250px", "hiddenSeries": false, "id": 21, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "Outbound", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "sum(rate(node_network_receive_bytes{cluster=\"$cluster\", device!=\"lo\"}[5m])) or sum(irate(node_network_receive_bytes{cluster=\"$cluster\", device!=\"lo\"}[5m])) or sum(max_over_time(rdsosmetrics_network_rx{cluster=\"$cluster\"}[5m])) or sum(max_over_time(rdsosmetrics_network_rx{cluster=\"$cluster\"}[5m])) ", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Inbound {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "sum(rate(node_network_transmit_bytes{cluster=\"$cluster\", device!=\"lo\"}[5m])) or sum(irate(node_network_transmit_bytes{cluster=\"$cluster\", device!=\"lo\"}[5m])) or\nsum(max_over_time(rdsosmetrics_network_tx{cluster=\"$cluster\"}[5m])) or sum(max_over_time(rdsosmetrics_network_tx{cluster=\"$cluster\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Outbound {{ instance_name }}", "metric": "", "refId": "A", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Network Traffic", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 2, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 144 }, "hiddenSeries": false, "id": 38, "legend": { "alignAsTable": false, "avg": true, "current": false, "hideEmpty": false, "max": false, "min": false, "rightSide": false, "show": true, "sort": "avg", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pswpin{cluster=\"$cluster\"}[5m]) * 4096 or irate(node_vmstat_pswpin{cluster=\"$cluster\"}[5m]) * 4096", "interval": "", "intervalFactor": 1, "legendFormat": "Swap In (Reads) {{ instance_name }}", "metric": "", "refId": "A", "step": 20, "target": "" }, { "calculatedInterval": "2s", "datasourceErrors": {}, "errors": {}, "expr": "rate(node_vmstat_pswpout{cluster=\"$cluster\"}[5m]) * 4096 or irate(node_vmstat_pswpout{cluster=\"$cluster\"}[5m]) * 4096", "interval": "", "intervalFactor": 1, "legendFormat": "Swap Out (Writes) {{ instance_name }}", "metric": "", "refId": "B", "step": 20, "target": "" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Swap Activity", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "Bps", "label": "", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "auto": true, "auto_count": 200, "auto_min": "1s", "current": { "selected": false, "text": "auto", "value": "$__auto_interval_interval" }, "datasource": "Prometheus", "description": null, "error": null, "hide": 2, "includeAll": false, "label": "Interval", "multi": false, "multiFormat": "glob", "name": "interval", "options": [ { "selected": true, "text": "auto", "value": "$__auto_interval_interval" }, { "selected": false, "text": "1s", "value": "1s" }, { "selected": false, "text": "5s", "value": "5s" }, { "selected": false, "text": "1m", "value": "1m" }, { "selected": false, "text": "5m", "value": "5m" }, { "selected": false, "text": "1h", "value": "1h" }, { "selected": false, "text": "6h", "value": "6h" }, { "selected": false, "text": "1d", "value": "1d" } ], "query": "1s,5s,1m,5m,1h,6h,1d", "queryValue": "", "refresh": 2, "skipUrlSync": false, "type": "interval" }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(mysql_up, cluster)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "集群", "multi": false, "name": "cluster", "options": [], "query": { "query": "label_values(mysql_up, cluster)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/23-tenginecluster-nginx-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 23, "uid": "9MOLXSbMz1", "title": "TengineCluster (Nginx) 信息面板", "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 1, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "nginx_server_connections{cluster=~\"$cluster\",status=~\"active|writing|reading|waiting\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{status}} {{instance_name}}", "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_cache{cluster=~\"$cluster\" }[5m])) by (status)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ status }} {{instance_name}}", "metric": "nginx_server_cache", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Cache", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "hiddenSeries": false, "id": 3, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_requests{cluster=~\"$cluster\", code!=\"total\"}[5m])) by (code)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ code }} {{ instance_name }}", "metric": "nginx_server_requests", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Requests", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 2, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_server_bytes{cluster=~\"$cluster\"}[5m])) by (direction)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ direction }} {{instance_name}}", "metric": "nginx_server_bytes", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Server Bytes", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": "0", "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "This one is providing aggregated error codes, but it's still possible to graph these per upstream.", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_upstream_requests{cluster=~\"$cluster\", upstream=~\"^$upstream$\",code!=\"total\"}[5m])) by (code)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ code }} {{ instance_name }}", "metric": "nginx_upstream_requests", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Requests", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 5, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(irate(nginx_upstream_bytes{cluster=~\"$cluster\", upstream=~\"^$upstream$\"}[5m])) by (direction)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ direction }} {{instance_name}}", "metric": "nginx_upstream_bytes", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Bytes", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 7, "w": 24, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 7, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(nginx_upstream_responseMsec{cluster=~\"$cluster\", upstream=~\"^$upstream$\"}) by (backend)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ backend }} {{instance_name}}", "metric": "nginx_upstream_response", "refId": "A", "step": 120 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Upstream Backend Response", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(nginx_server_bytes,cluster)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "集群", "multi": false, "name": "cluster", "options": [], "query": { "query": "label_values(nginx_server_bytes,cluster)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": ".*", "current": {}, "datasource": "Prometheus", "definition": "label_values(nginx_upstream_bytes{cluster=\"$cluster\"}, upstream)", "description": null, "error": null, "hide": 0, "includeAll": true, "label": "upstream", "multi": false, "name": "upstream", "options": [], "query": { "query": "label_values(nginx_upstream_bytes{cluster=\"$cluster\"}, upstream)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/24-victoriametrics-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 24, "uid": "wNf0q_kZk", "title": "victoriametrics 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 6, "panels": [], "title": "Stats", "type": "row" }, { "datasource": "Prometheus", "description": "", "gridPos": { "h": 2, "w": 4, "x": 0, "y": 1 }, "id": 85, "options": { "content": "
$version
", "mode": "markdown" }, "pluginVersion": "8.3.2", "title": "Version", "type": "text" }, { "datasource": "Prometheus", "description": "How many datapoints are in storage", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 4, "y": 1 }, "id": 26, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!=\"indexdb\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Total datapoints", "type": "stat" }, { "datasource": "Prometheus", "description": "Total amount of used disk space", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 9, "y": 1 }, "id": 81, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "datasource": "Prometheus", "exemplar": false, "expr": "sum(vm_data_size_bytes{job=~\"$job\", type!=\"indexdb\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Disk space usage", "type": "stat" }, { "datasource": "Prometheus", "description": "Average disk usage per datapoint.", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 14, "y": 1 }, "id": 82, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "datasource": "Prometheus", "exemplar": false, "expr": "sum(vm_data_size_bytes{job=~\"$job\", type!=\"indexdb\"}) / sum(vm_rows{job=~\"$job\", type!=\"indexdb\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Bytes per point", "type": "stat" }, { "datasource": "Prometheus", "description": "Total size of allowed memory via flag `-memory.allowedPercent`", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 19, "y": 1 }, "id": 79, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "sum(vm_allowed_memory_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Allowed memory", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "red", "value": null }, { "color": "green", "value": 1800 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 2, "w": 4, "x": 0, "y": 3 }, "id": 87, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "vm_app_uptime_seconds{job=~\"$job\", instance=~\"$instance\"}", "instant": true, "interval": "", "legendFormat": "", "refId": "A" } ], "title": "Uptime", "type": "stat" }, { "datasource": "Prometheus", "description": "How many entries inverted index contains. This value is proportional to the number of unique timeseries in storage(cardinality).", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 4, "y": 3 }, "id": 38, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Index size", "type": "stat" }, { "datasource": "Prometheus", "description": "Total size of available memory for VM process", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 9, "y": 3 }, "id": 78, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "sum(vm_available_memory_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Available memory", "type": "stat" }, { "datasource": "Prometheus", "description": "The minimum free disk space left", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "percentage", "steps": [ { "color": "green", "value": null } ] }, "unit": "bytes" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 14, "y": 3 }, "id": 80, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "min(vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Min free disk space", "type": "stat" }, { "datasource": "Prometheus", "description": "Total number of available CPUs for VM process", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "short" }, "overrides": [] }, "gridPos": { "h": 2, "w": 5, "x": 19, "y": 3 }, "id": 77, "links": [], "maxDataPoints": 100, "options": { "colorMode": "value", "graphMode": "area", "justifyMode": "auto", "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "8.3.2", "targets": [ { "exemplar": true, "expr": "sum(vm_available_cpu_cores{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "instant": true, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Available CPU", "type": "stat" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 5 }, "id": 24, "panels": [], "title": "Performance", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 6 }, "hiddenSeries": false, "id": 12, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_http_requests_total{job=~\"$job\", instance=~\"$instance\", path!~\"/favicon.ico\"}[1m])) by (path) > 0", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{path}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Requests rate ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The less time it takes is better.\n* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 6 }, "hiddenSeries": false, "id": 22, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "max", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "go_gc_duration_seconds_sum{job=~\"$job\", instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "gc_duration_seconds_sum ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "s", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the number of active time series with new data points inserted during the last hour. High value may result in ingestion slowdown. \n\nSee following link for details:", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 51, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [ { "targetBlank": true, "title": "troubleshooting", "url": "https://github.com/VictoriaMetrics/VictoriaMetrics/blob/master/README.md#troubleshooting" } ], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "vm_cache_entries{job=~\"$job\", instance=~\"$instance\", type=\"storage/hour_metric_ids\"}", "format": "time_series", "intervalFactor": 1, "legendFormat": "Active time series", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Active time series ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "VictoriaMetrics stores various caches in RAM. Memory size for these caches may be limited with -`memory.allowedPercent` flag. Line `max allowed` shows max allowed memory size for cache.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 33, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "max allowed", "color": "#C4162A" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "size", "refId": "A" }, { "expr": "max(vm_allowed_memory_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "max allowed", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Cache size ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows how many ongoing insertions (not API /write calls) on disk are taking place, where:\n* `max` - equal to number of CPUs;\n* `current` - current number of goroutines busy with inserting rows into underlying storage.\n\nEvery successful API /write call results into flush on disk. However, these two actions are separated and controlled via different concurrency limiters. The `max` on this panel can't be changed and always equal to number of CPUs. \n\nWhen `current` hits `max` constantly, it means storage is overloaded and requires more CPU.\n\n", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 22 }, "hiddenSeries": false, "id": 59, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideEmpty": false, "hideZero": false, "max": false, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "max", "color": "#C4162A" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_concurrent_addrows_capacity{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "max", "refId": "A" }, { "expr": "sum(vm_concurrent_addrows_current{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "intervalFactor": 1, "legendFormat": "current", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Concurrent flushes on disk ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true }, { "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "* `*` - unsupported query path\n* `/write` - insert into VM\n* `/metrics` - query VM system metrics\n* `/query` - query instant values\n* `/query_range` - query over a range of time\n* `/series` - match a certain label set\n* `/label/{}/values` - query a list of label values (variables mostly)", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 22 }, "hiddenSeries": false, "id": 35, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": false, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_http_request_errors_total{job=~\"$job\", instance=~\"$instance\"}[1m])) by (path) > 0", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{path}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Requests error rate ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 30 }, "id": 14, "panels": [], "title": "Storage", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows how many datapoints are in the storage and what is average disk usage per datapoint.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 31 }, "hiddenSeries": false, "id": 30, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "bytes-per-datapoint", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type != \"indexdb\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "total datapoints", "refId": "A" }, { "expr": "sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!=\"indexdb\"}) / sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type != \"indexdb\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "bytes-per-datapoint", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Datapoints ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "decimals": 2, "format": "bytes", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the time needed to reach the 100% of disk capacity based on the following params:\n* free disk space;\n* row ingestion rate;\n* dedup rate;\n* compression.\n\nUse this panel for capacity planning in order to estimate the time remaining for running out of the disk space.\n\n", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 31 }, "hiddenSeries": false, "id": 73, "legend": { "alignAsTable": true, "avg": true, "current": true, "hideZero": true, "max": false, "min": true, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null as zero", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"} / ignoring(path) ((rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[1d]) - ignoring(type) rate(vm_deduplicated_samples_total{job=~\"$job\", instance=~\"$instance\", type=\"merge\"}[1d])) * scalar(sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!=\"indexdb\"}) / sum(vm_rows{job=~\"$job\", instance=~\"$instance\", type!=\"indexdb\"})))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Storage full ETA ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "s", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows amount of on-disk space occupied by data points and the remaining disk space at `-storageDataPath`", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 39 }, "hiddenSeries": false, "id": 53, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "rightSide": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type!=\"indexdb\"})", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Used", "refId": "A" }, { "expr": "vm_free_disk_space_bytes{job=~\"$job\", instance=~\"$instance\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "Free", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Disk space usage - datapoints ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "How many datapoints are in RAM queue waiting to be written into storage. The number of pending data points should be in the range from 0 to `2*`, since VictoriaMetrics pushes pending data to persistent storage every second.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 39 }, "hiddenSeries": false, "id": 34, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "pending index entries", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "vm_pending_rows{job=~\"$job\", instance=~\"$instance\", type=\"storage\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "pending datapoints", "refId": "A" }, { "expr": "vm_pending_rows{job=~\"$job\", instance=~\"$instance\", type=\"indexdb\"}", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "pending index entries", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Pending datapoints ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "decimals": 3, "format": "none", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows amount of on-disk space occupied by inverted index.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 47 }, "hiddenSeries": false, "id": 55, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "exemplar": true, "expr": "vm_data_size_bytes{job=~\"$job\", instance=~\"$instance\", type=\"indexdb\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "disk space used", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Disk space usage - index ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Data parts of LSM tree.\nHigh number of parts could be an evidence of slow merge performance - check the resource utilization.\n* `indexdb` - inverted index\n* `storage/small` - recently added parts of data ingested into storage(hot data)\n* `storage/big` - small parts gradually merged into big parts (cold data)", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 47 }, "hiddenSeries": false, "id": 36, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_parts{job=~\"$job\", instance=~\"$instance\"}) by (type)", "format": "time_series", "intervalFactor": 1, "legendFormat": "{{type}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "LSM parts ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows how many rows were ignored on insertion due to corrupted or out of retention timestamps.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 55 }, "hiddenSeries": false, "id": 58, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "exemplar": true, "expr": "sum(vm_rows_ignored_total{job=~\"$job\", instance=~\"$instance\"}) by (reason)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{reason}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Rows ignored ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The number of on-going merges in storage nodes. It is expected to have high numbers for `storage/small` metric.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 55 }, "hiddenSeries": false, "id": 62, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(vm_active_merges{job=~\"$job\", instance=~\"$instance\"}) by(type)", "legendFormat": "{{type}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Active merges ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the rate of logging the messages by their level. Unexpected spike in rate is a good reason to check logs.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 63 }, "hiddenSeries": false, "id": 67, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(vm_log_messages_total{job=~\"$job\", instance=~\"$instance\"}[5m])) by (level) ", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "{{level}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Logging rate ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The number of rows merged per second by storage nodes.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 63 }, "hiddenSeries": false, "id": 64, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(vm_rows_merged_total{job=~\"$job\", instance=~\"$instance\"}[5m])) by(type)", "legendFormat": "{{type}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Merge speed ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:867", "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true }, { "$$hashKey": "object:868", "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 71 }, "id": 71, "panels": [], "title": "Troubleshooting", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The percentage of slow inserts comparing to total insertion rate during the last 5 minutes. \n\nThe less value is better. If percentage remains high (>50%) during extended periods of time, then it is likely more RAM is needed for optimal handling of the current number of active time series. \n\nIn general, VictoriaMetrics requires ~1KB or RAM per active time series, so it should be easy calculating the required amounts of RAM for the current workload according to capacity planning docs. But the resulting number may be far from the real number because the required amounts of memory depends on may other factors such as the number of labels per time series and the length of label values.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 72 }, "hiddenSeries": false, "id": 10, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_slow_row_inserts_total{job=~\"$job\", instance=~\"$instance\"}[5m])) / sum(rate(vm_rows_added_to_storage_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "慢写入所占比例", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "慢写入 ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 2, "format": "percentunit", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Slow queries rate according to `search.logSlowQueryDuration` flag, which is `5s` by default.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 72 }, "hiddenSeries": false, "id": 60, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_slow_queries_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "slow queries rate", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Slow queries rate ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the percentage of used cache size from the allowed size by type. \nValues close to 100% show the maximum potential utilization.\nValues close to 0% show that cache is underutilized.", "fill": 0, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 80 }, "hiddenSeries": false, "id": 90, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"} / vm_cache_size_max_bytes{job=~\"$job\", instance=~\"$instance\"}", "interval": "", "legendFormat": "{{type}}", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Cache usage % ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:235", "format": "percentunit", "logBase": 1, "show": true }, { "$$hashKey": "object:236", "format": "short", "logBase": 1, "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the rate and total number of new series created over last 24h.\n\nHigh churn rate tightly connected with database performance and may result in unexpected OOM's or slow queries. It is recommended to always keep an eye on this metric to avoid unexpected cardinality \"explosions\".\n\nThe higher churn rate is, the more resources required to handle it. Consider to keep the churn rate as low as possible.\n\nGood references to read:\n* https://www.robustperception.io/cardinality-is-key\n* https://www.robustperception.io/using-tsdb-analyze-to-investigate-churn-and-cardinality", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 81 }, "hiddenSeries": false, "id": 66, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "new series over 24h", "yaxis": 2 } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "interval": "", "legendFormat": "churn rate", "refId": "A" }, { "expr": "sum(increase(vm_new_timeseries_created_total{job=~\"$job\", instance=~\"$instance\"}[24h]))", "interval": "", "legendFormat": "new series over 24h", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Churn rate ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The percentage of slow rows read comparing to total slow blocks read rate during the last 5 minutes. \n\nThe less value is better. If percentage remains high (>50%) during extended periods of time, then it is likely more RAM is needed for optimal handling of the current number of active time series. \n\nIn general, VictoriaMetrics requires ~1KB or RAM per active time series, so it should be easy calculating the required amounts of RAM for the current workload according to capacity planning docs. But the resulting number may be far from the real number because the required amounts of memory depends on may other factors such as the number of labels per time series and the length of label values.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 89 }, "hiddenSeries": false, "id": 74, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_vmselect_metric_rows_read_total{job=~\"$job\", instance=~\"$instance\"}[5m])) / sum(rate(vm_vmselect_metric_blocks_read_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "慢查询所占比例", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "慢查询 ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 2, "format": "percentunit", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows whether background merges were deferred because of not enough of disk space. \n\nBefore starting the data parts merge (improves compression and reduces number of files), VictoriaMetrics estimates if there is enough of disk space for the operation. If panel shows values higher than zero, it means no disk space left for merging operation.\n\nConsider extending the disk size or decreasing retention. ", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 89 }, "hiddenSeries": false, "id": 88, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": false, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(vm_merge_need_free_disk_space{job=~\"$job\", instance=~\"$instance\"}) by(type)", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "{{ type }} deferred", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Merges deferred ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 2, "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 98 }, "id": 46, "panels": [], "title": "Resource usage", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 99 }, "hiddenSeries": false, "id": 44, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(go_memstats_sys_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "requested from system", "refId": "A" }, { "expr": "sum(go_memstats_heap_inuse_bytes{job=~\"$job\", instance=~\"$instance\"}) + sum(vm_cache_size_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "heap inuse", "refId": "B" }, { "expr": "sum(go_memstats_stack_inuse_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "intervalFactor": 1, "legendFormat": "stack inuse", "refId": "C" }, { "expr": "sum(process_resident_memory_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "resident", "refId": "D" }, { "exemplar": true, "expr": "sum(process_resident_memory_anon_bytes{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "resident anonymous", "refId": "E" } ], "thresholds": [], "timeRegions": [], "title": "Memory usage ($instance)", "tooltip": { "shared": true, "sort": 2, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 99 }, "hiddenSeries": false, "id": 57, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(process_cpu_seconds_total{job=~\"$job\", instance=~\"$instance\"}[5m])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "CPU cores used", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "CPU ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Panel shows the number of open file descriptors in the OS.\nReaching the limit of open files can cause various issues and must be prevented.\n\nSee how to change limits here https://medium.com/@muhammadtriwibowo/set-permanently-ulimit-n-open-files-in-ubuntu-4d61064429a", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 107 }, "hiddenSeries": false, "id": 75, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "max", "color": "#C4162A" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(process_open_fds{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "open", "refId": "A" }, { "expr": "min(process_max_fds{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "max", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Open FDs ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "logBase": 2, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows the number of bytes read/write from the storage layer.", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 107 }, "hiddenSeries": false, "id": 76, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "read", "transform": "negative-Y" } ], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(process_io_storage_read_bytes_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "read", "refId": "A" }, { "datasource": "Prometheus", "expr": "sum(rate(process_io_storage_written_bytes_total{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "write", "refId": "B" } ], "thresholds": [], "timeRegions": [], "title": "Disk writes/reads ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 115 }, "hiddenSeries": false, "id": 47, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(go_goroutines{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "intervalFactor": 2, "legendFormat": "gc duration", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Goroutines ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "Shows avg GC duration", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 115 }, "hiddenSeries": false, "id": 42, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(go_gc_duration_seconds_sum{job=~\"$job\", instance=~\"$instance\"}[5m]))\n/\nsum(rate(go_gc_duration_seconds_count{job=~\"$job\", instance=~\"$instance\"}[5m]))", "format": "time_series", "intervalFactor": 2, "legendFormat": "avg gc duration", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "GC duration ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "s", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 123 }, "hiddenSeries": false, "id": 48, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(process_num_threads{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "intervalFactor": 2, "legendFormat": "threads", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "Threads ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "decimals": 0, "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 123 }, "hiddenSeries": false, "id": 37, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(vm_tcplistener_conns{job=~\"$job\", instance=~\"$instance\"})", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "connections", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "TCP connections ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "", "fieldConfig": { "defaults": { "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 131 }, "hiddenSeries": false, "id": 49, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": false, "show": true, "sort": "current", "sortDesc": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "8.3.2", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "datasource": "Prometheus", "exemplar": true, "expr": "sum(rate(vm_tcplistener_accepts_total{job=~\"$job\", instance=~\"$instance\"}[1m]))", "format": "time_series", "hide": false, "interval": "", "intervalFactor": 1, "legendFormat": "connections", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "TCP connections rate ($instance)", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "mode": "time", "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "min": "0", "show": true }, { "format": "short", "logBase": 1, "min": "0", "show": true } ], "yaxis": { "align": false } } ], "style": "dark", "refresh": false, "schemaVersion": 33, "tags": [ "victoriametrics", "vmsingle" ], "templating": { "list": [ { "allFormat": "glob", "current": { "selected": false, "text": "default", "value": "default" }, "datasource": "Prometheus", "definition": "label_values(vm_app_version, env)", "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(vm_app_version, env)", "refId": "VictoriaMetrics-job-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "victoriaMetricsExporter", "value": "victoriaMetricsExporter" }, "datasource": "Prometheus", "definition": "label_values(vm_app_version{version=~\"vmstorage.*\"}, job)", "hide": 0, "includeAll": false, "multi": false, "name": "job", "options": [], "query": { "query": "label_values(vm_app_version{version=~\"vmstorage.*\"}, job)", "refId": "VictoriaMetrics-job-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "v1.71.0", "value": "v1.71.0" }, "datasource": "Prometheus", "definition": "label_values(vm_app_version{job=~\"$job\", instance=~\"$instance\"}, version)", "hide": 2, "includeAll": false, "multi": false, "name": "version", "options": [], "query": { "query": "label_values(vm_app_version{job=~\"$job\", instance=~\"$instance\"}, version)", "refId": "VictoriaMetrics-version-Variable-Query" }, "refresh": 1, "regex": "/.*-tags-(v\\d+\\.\\d+\\.\\d+)/", "skipUrlSync": false, "sort": 2, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false }, { "current": { "selected": false, "text": "10.0.16.167", "value": "10.0.16.167" }, "datasource": "Prometheus", "definition": "label_values(vm_app_version{job=~\"$job\"}, instance)", "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(vm_app_version{job=~\"$job\"}, instance)", "refId": "VictoriaMetrics-instance-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tagsQuery": "", "type": "query", "useTags": false } ] }, "time": { "from": "now-30m", "to": "now" }, "timepicker": { "refresh_intervals": [ "5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d" ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/25-rocketmq-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 25, "uid": "fzr_Fmd4k", "title": "rocketmq 信息面板", "panels": [ { "datasource": "Prometheus", "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "fill": 1, "gridPos": { "h": 6, "w": 7, "x": 0, "y": 0 }, "id": 12, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_broker_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_broker_qps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "broker tps & broker qps", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 6, "w": 9, "x": 7, "y": 0 }, "id": 8, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_consumer_offset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_consumer_offset", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 }, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_consumer_message_size{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_consumer_message_size", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "fill": 1, "gridPos": { "h": 7, "w": 7, "x": 0, "y": 6 }, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_producer_offset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_producer_offset", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "消费tps", "fill": 1, "gridPos": { "h": 7, "w": 9, "x": 7, "y": 6 }, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_consumer_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_consumer_tps", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 6 }, "id": 14, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_producer_message_size{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_producer_message_size", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 6, "w": 7, "x": 0, "y": 13 }, "id": 2, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_producer_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "hide": false, "instant": false, "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_producer_tps", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 6, "w": 9, "x": 7, "y": 13 }, "id": 10, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_group_get_latency_by_storetime{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_group_get_latency_by_storetime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 6, "w": 8, "x": 16, "y": 13 }, "id": 18, "legend": { "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rocketmq_producer_offset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}) by (topic) - on(topic) group_right sum(rocketmq_consumer_offset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}) by (group,topic)", "format": "time_series", "intervalFactor": 1, "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "rocketmq_message_accumulation", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 7, "w": 8, "x": 0, "y": 19 }, "id": 20, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_brokeruntime_pmdt_0ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_brokeruntime_pmdt_0to10ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_brokeruntime_pmdt_10to50ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" }, { "expr": "rocketmq_brokeruntime_pmdt_50to100ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "D" }, { "expr": "rocketmq_brokeruntime_pmdt_100to200ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "E" }, { "expr": "rocketmq_brokeruntime_pmdt_200to500ms{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "F" }, { "expr": "rocketmq_brokeruntime_pmdt_500to1s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "G" }, { "expr": "rocketmq_brokeruntime_pmdt_1to2s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "H" }, { "expr": "rocketmq_brokeruntime_pmdt_2to3s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "I" }, { "expr": "rocketmq_brokeruntime_pmdt_3to4s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "J" }, { "expr": "rocketmq_brokeruntime_pmdt_4to5s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "K" }, { "expr": "rocketmq_brokeruntime_pmdt_5to10s{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "L" }, { "expr": "rocketmq_brokeruntime_pmdt_10stomore{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "M" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "PutMessageDistributeTime", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "decimals": null, "format": "short", "label": "", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 7, "w": 8, "x": 8, "y": 19 }, "id": 28, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_brokeruntime_pull_threadpoolqueue_headwait_timemills{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_brokeruntime_query_threadpoolqueue_headwait_timemills{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_brokeruntime_send_threadpoolqueue_headwait_timemills{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "threadpoolqueue_headwait_timemills", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 7, "w": 8, "x": 16, "y": 19 }, "id": 30, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_client_consume_fail_msg_count{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_client_consume_fail_msg_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_client_consume_ok_msg_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" }, { "expr": "rocketmq_client_consume_rt{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "D" }, { "expr": "rocketmq_client_consumer_pull_rt{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "E" }, { "expr": "rocketmq_client_consumer_pull_tps{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "F" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "consume client info", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 8, "w": 8, "x": 0, "y": 26 }, "id": 26, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_brokeruntime_getfound_tps10{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_brokeruntime_gettotal_tps10{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_brokeruntime_gettransfered_tps10{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" }, { "expr": "rocketmq_brokeruntime_getmiss_tps10{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "D" }, { "expr": "rocketmq_brokeruntime_put_tps10{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "E" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "runtime tps", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 8, "w": 8, "x": 8, "y": 26 }, "id": 24, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_brokeruntime_commitlog_disk_ratio{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_brokeruntime_consumequeue_disk_ratio{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_brokeruntime_commitlogdir_capacity_free{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" }, { "expr": "rocketmq_brokeruntime_commitlogdir_capacity_total{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "D" }, { "expr": "rocketmq_brokeruntime_commitlog_maxoffset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "E" }, { "expr": "rocketmq_brokeruntime_commitlog_minoffset{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "F" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "disk space", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fill": 1, "gridPos": { "h": 8, "w": 8, "x": 16, "y": 26 }, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": false, "total": false, "values": false }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "percentage": false, "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocketmq_brokeruntime_msg_put_total_today_now{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "A" }, { "expr": "rocketmq_brokeruntime_msg_gettotal_today_now{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "B" }, { "expr": "rocketmq_brokeruntime_dispatch_behind_bytes{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "C" }, { "expr": "rocketmq_brokeruntime_put_message_size_total{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "D" }, { "expr": "rocketmq_brokeruntime_put_message_average_size{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "E" }, { "expr": "rocketmq_brokeruntime_msg_gettotal_yesterdaymorning{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "F" }, { "expr": "rocketmq_brokeruntime_msg_puttotal_yesterdaymorning{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "G" }, { "expr": "rocketmq_brokeruntime_msg_gettotal_todaymorning{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "H" }, { "expr": "rocketmq_brokeruntime_msg_puttotal_todaymorning{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "format": "time_series", "intervalFactor": 1, "refId": "I" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "broker runtime info", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(rocketmq_topic_retry_offset,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(rocketmq_topic_retry_offset,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(rocketmq_topic_retry_offset{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(rocketmq_topic_retry_offset{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(rocketmq_topic_retry_offset,job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(rocketmq_topic_retry_offset,job)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/3-mian-ban-lie-biao.json ================================================ { "dashboard": { "id": 3, "uid": "XrwAXz_Mz", "title": "面板列表", "panels": [ { "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "gridPos": { "h": 20, "w": 24, "x": 0, "y": 0 }, "id": 2, "options": { "folderId": null, "maxItems": 100, "query": "", "showHeadings": false, "showRecentlyViewed": false, "showSearch": true, "showStarred": false, "tags": [] }, "pluginVersion": "7.4.3", "timeFrom": null, "timeShift": null, "title": "Dashboard List", "type": "dashlist" } ], "templating": { "list": [] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/4-app-logs.json ================================================ { "dashboard": { "id": 4, "uid": "liz0yRCZz", "title": "AppLogs", "panels": [ { "aliasColors": {}, "bars": true, "cacheTimeout": null, "dashLength": 10, "dashes": false, "datasource": "Loki", "decimals": null, "fieldConfig": { "defaults": { "custom": {}, "links": [], "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 6, "interval": null, "legend": { "alignAsTable": false, "avg": true, "current": true, "hideEmpty": false, "hideZero": false, "max": true, "min": true, "rightSide": false, "show": true, "total": true, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(count_over_time({job=\"$app\",filename=\"$filename\",env=\"$env\",instance=\"$instance\"}[$__interval]))", "legendFormat": "{{ log }}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "datasource": "Loki", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "gridPos": { "h": 25, "w": 24, "x": 0, "y": 8 }, "id": 2, "maxDataPoints": "", "options": { "showLabels": false, "showTime": false, "sortOrder": "Descending", "wrapLogMessage": false }, "pluginVersion": "7.2.2", "targets": [ { "expr": "{job=\"$app\",filename=\"$filename\",env=\"$env\",instance=\"$instance\"} |= \"$search\"", "hide": false, "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "", "transparent": true, "type": "logs" } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Loki", "definition": "label_values(env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": "label_values(env)", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Loki", "definition": "label_values({env=\"$env\"},job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "app", "multi": false, "name": "app", "options": [], "query": "label_values({env=\"$env\"},job)", "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Loki", "definition": "label_values({job=\"$app\",env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": "label_values({job=\"$app\",env=\"$env\"},instance)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Loki", "definition": "label_values({job=\"$app\",env=\"$env\",instance=\"$instance\"},filename)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "filename", "multi": false, "name": "filename", "options": [], "query": "label_values({job=\"$app\",env=\"$env\",instance=\"$instance\"},filename)", "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "type": "query", "useTags": false }, { "current": { "selected": true, "text": "", "value": "" }, "description": null, "error": null, "hide": 0, "label": "String Match", "name": "search", "options": [ { "selected": true, "text": "", "value": "" } ], "query": "", "skipUrlSync": false, "type": "textbox" } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/5-arangodb-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 5, "uid": "jtXDTgPMk", "title": "Arangodb 信息面板", "panels": [ { "datasource": "Prometheus", "description": "rocksdb base level", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 0 }, "id": 26, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rocksdb_base_level{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "RocksdbBaseLevel", "type": "gauge" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "The number of client connections that are currently open.", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 0 }, "hiddenSeries": false, "id": 4, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "arangodb_client_connection_statistics_client_connections{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ClientConnections", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "datasource": "Prometheus", "description": "Amount of time that this process has been scheduled in kernel mode, measured in seconds.", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "id": 6, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "arangodb_process_statistics_system_time{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "UseSystemTime", "type": "stat" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "Amount of time that this process has been scheduled in user mode, measured in seconds.", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 0 }, "id": 8, "interval": null, "links": [], "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "arangodb_process_statistics_user_time{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "UseUserTime", "type": "stat" }, { "datasource": "Prometheus", "description": "rocksdb background errors", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 0, "y": 4 }, "id": 24, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rocksdb_background_errors{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "RocksdbBackgroundErrors", "type": "stat" }, { "datasource": "Prometheus", "description": "Transactions started", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "#6ED0E0", "value": 100 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 6, "y": 4 }, "id": 12, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "arangodb_transactions_started{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "ArangodbTransactionsStarted", "type": "stat" }, { "datasource": "Prometheus", "description": "Number of threads in the arangod process.", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 4 }, "id": 2, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "arangodb_process_statistics_number_of_threads{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "instant": false, "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "ThreadNumbers", "type": "stat" }, { "datasource": "Prometheus", "description": "rocksdb cache limit", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "#BA43A9", "value": 50 } ] }, "unit": "decbytes" }, "overrides": [] }, "gridPos": { "h": 4, "w": 6, "x": 18, "y": 4 }, "id": 20, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rocksdb_cache_limit{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "RocksdbCacheLimit", "type": "stat" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "rocksdb size all mem tables", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] }, "unit": "decbytes" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "hiddenSeries": false, "id": 32, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocksdb_size_all_mem_tables{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RocksdbSizeAllMemTables", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "rocksdb_cache_allocated", "fieldConfig": { "defaults": { "custom": {}, "unit": "decbytes" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "hiddenSeries": false, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocksdb_cache_allocated{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RocksdbCacheAllocated", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "rocksdb snapshots num", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 30, "interval": null, "links": [], "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rocksdb_num_snapshots{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "RocksdbNumSnapshots", "type": "gauge" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "decimals": null, "description": "Transactions committed", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "hiddenSeries": false, "id": 10, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "arangodb_transactions_committed{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ArangodbTransactionsCommitted", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "rocksdb estimate num keys", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 }, "hiddenSeries": false, "id": 28, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocksdb_estimate_num_keys{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RocksdbEstimateNumKeys ", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "rocksdb actual delayed write rate", "fieldConfig": { "defaults": { "custom": {}, "unit": "percent" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 }, "hiddenSeries": false, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocksdb_actual_delayed_write_rate{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RocksdbActualDelayedWriteRate", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "rocksdb cache hit rate recent", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] }, "unit": "percent" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 32 }, "hiddenSeries": false, "id": 18, "legend": { "avg": false, "current": false, "hideEmpty": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rocksdb_cache_hit_rate_recent{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RocksdbCacheHitRateRecent", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percent", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "ransactions aborted", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 32 }, "hiddenSeries": false, "id": 14, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "arangodb_transactions_aborted{env=~\"$env\",instance=~\"$instance\",job=\"$job\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ArangodbTransactionsAborted", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(arangodb_client_connection_statistics_client_connections,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(arangodb_client_connection_statistics_client_connections,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values(arangodb_client_connection_statistics_client_connections{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(arangodb_client_connection_statistics_client_connections{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { "selected": false, "text": "arangodbExporter", "value": "arangodbExporter" }, "datasource": "Prometheus", "definition": "label_values(arangodb_client_connection_statistics_client_connections,job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(arangodb_client_connection_statistics_client_connections,job)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/6-beanstalkdxin-xi-mian-ban.json ================================================ { "dashboard": { "id": 6, "uid": "TBAkOmyMz", "title": "Beanstalkd 信息面板", "panels": [ { "datasource": "Prometheus", "description": "up time", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "id": 2, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "uptime{job=\"beanstalkExporter\",env=~\"$env\",instance=~\"$instance\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "UpTime", "type": "stat" }, { "datasource": "Prometheus", "description": "sys cpu time", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 6, "y": 0 }, "id": 8, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rusage_stime{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "SysCpuTime", "type": "stat" }, { "datasource": "Prometheus", "description": "user cpu time", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 12, "y": 0 }, "id": 10, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "rusage_utime{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "UserCpuTime", "type": "stat" }, { "cacheTimeout": null, "datasource": "Prometheus", "description": "total connections", "fieldConfig": { "defaults": { "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] } }, "overrides": [] }, "gridPos": { "h": 3, "w": 6, "x": 18, "y": 0 }, "id": 4, "interval": null, "links": [], "options": { "displayMode": "gradient", "orientation": "auto", "reduceOptions": { "calcs": [ "mean" ], "fields": "", "values": false }, "showUnfilled": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "total_connections{job=\"beanstalkExporter\",env=~\"$env\",instance=~\"$instance\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "TotalConnections", "type": "bargauge" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "total jobs", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 3 }, "hiddenSeries": false, "id": 6, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "total_jobs{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "TotalJobs", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "buried jobs", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 3 }, "hiddenSeries": false, "id": 34, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_jobs_buried{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "BuriedJobs", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "delayed jobs", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 11 }, "hiddenSeries": false, "id": 32, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "current_jobs_delayed{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "DelayedJobs", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of timeout job", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 11 }, "hiddenSeries": false, "id": 30, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "job_timeouts{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "TimeoutJobNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of stats cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 19 }, "hiddenSeries": false, "id": 28, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_stats{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "StatsCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of reverse cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 19 }, "hiddenSeries": false, "id": 26, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_reserve{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ReverseCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of release cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 27 }, "hiddenSeries": false, "id": 24, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_release{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "ReleaseCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of put cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 27 }, "hiddenSeries": false, "id": 22, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_put{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "PutCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of peek cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 35 }, "hiddenSeries": false, "id": 20, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_peek{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "PeekCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of kick cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 35 }, "hiddenSeries": false, "id": 18, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_kick{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "KickCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of ignore cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 43 }, "hiddenSeries": false, "id": 16, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_ignore{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "IgnoreCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of delete cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 43 }, "hiddenSeries": false, "id": 14, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_delete{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "DeleteCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "description": "num of bury cmd ", "fieldConfig": { "defaults": { "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 51 }, "hiddenSeries": false, "id": 12, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "cmd_bury{env=~\"$env\",instance=\"$instance\",job=\"beanstalkExporter\"}", "interval": "", "legendFormat": "", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "BuryCmdNum", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(total_jobs,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(total_jobs,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values({job=\"beanstalkExporter\",env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values({job=\"beanstalkExporter\",env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/7-clickhouse-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 7, "uid": "VqK1fIBMk", "title": "Clickhouse 信息面板", "panels": [ { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 0 }, "hiddenSeries": false, "id": 1, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": true, "steppedLine": false, "targets": [ { "expr": "rate(clickhouse_query{env=~\"$env\",instance=\"$instance\",job=\"$job\"}[1m])", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Query", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 0 }, "hiddenSeries": false, "id": 2, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [ { "alias": "/merge.*/", "bars": false, "lines": true }, { "alias": "/rate.*/", "bars": true, "lines": false, "yaxis": 2, "zindex": -1 } ], "spaceLength": 10, "stack": false, "steppedLine": true, "targets": [ { "expr": "clickhouse_merge{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "merge {{job}}", "refId": "A", "step": 4 }, { "expr": "rate(clickhouse_merged_rows_total{env=~\"$env\",instance=\"$instance\",job=\"$job\"}[1m])", "interval": "", "intervalFactor": 2, "legendFormat": "rate {{job}}", "refId": "B", "step": 4 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Merge", "tooltip": { "msResolution": false, "shared": true, "sort": 2, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "cacheTimeout": null, "colorBackground": false, "colorValue": true, "colors": [ "rgba(50, 172, 45, 0.97)", "rgba(237, 129, 40, 0.89)", "rgba(245, 54, 54, 0.9)" ], "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "format": "none", "gauge": { "maxValue": 100, "minValue": 0, "show": false, "thresholdLabels": false, "thresholdMarkers": true }, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 7 }, "id": 6, "interval": null, "isNew": true, "links": [], "mappingType": 1, "mappingTypes": [ { "name": "value to text", "value": 1 }, { "name": "range to text", "value": 2 } ], "maxDataPoints": 100, "nullPointMode": "connected", "nullText": null, "postfix": "", "postfixFontSize": "50%", "prefix": "", "prefixFontSize": "50%", "rangeMaps": [ { "from": "null", "text": "N/A", "to": "null" } ], "sparkline": { "fillColor": "rgba(31, 118, 189, 0.18)", "full": false, "lineColor": "rgb(31, 120, 193)", "show": false }, "tableColumn": "", "targets": [ { "expr": "sum(clickhouse_readonly_replica{env=~\"$env\",instance=\"$instance\",job=\"$job\"})", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 60 } ], "thresholds": "1,1", "title": "ReadOnly replica", "type": "singlestat", "valueFontSize": "80%", "valueMaps": [ { "op": "=", "text": "N/A", "value": "null" } ], "valueName": "current" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 7 }, "hiddenSeries": false, "id": 3, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_replicated_checks{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 20 }, { "expr": "clickhouse_replicated_fetch{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "B", "step": 20 }, { "expr": "clickhouse_replicated_send{instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "C", "step": 20 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Replication", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 4, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_read{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 }, { "expr": "clickhouse_write{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "B", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Read/Write", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 9, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_background_pool_task{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "{{job}}", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Pool Tasks", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 0, "y": 21 }, "hiddenSeries": false, "id": 5, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_tcp_connection{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "tcp {{instance}}", "refId": "A", "step": 10 }, { "expr": "clickhouse_http_connection{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "http {{instance}}", "refId": "B", "step": 10 }, { "expr": "clickhouse_interserver_connection{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "interserver", "refId": "C", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Connections", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "unit": "decbytes" }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 7, "w": 12, "x": 12, "y": 21 }, "hiddenSeries": false, "id": 10, "isNew": true, "legend": { "avg": true, "current": true, "hideEmpty": true, "hideZero": true, "max": false, "min": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "clickhouse_memory_tracking{env=~\"$env\",instance=\"$instance\",job=\"$job\"}", "interval": "", "intervalFactor": 2, "legendFormat": "", "refId": "A", "step": 10 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Memory", "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "decbytes", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(clickhouse_query,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(clickhouse_query,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(clickhouse_query{env=\"$env\"},instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "instance", "multi": false, "name": "instance", "options": [], "query": { "query": "label_values(clickhouse_query{env=\"$env\"},instance)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(clickhouse_query,job)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "job", "multi": false, "name": "job", "options": [], "query": { "query": "label_values(clickhouse_query,job)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/8-elasticsearch-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 8, "uid": "qXG-qEFMk", "title": "ElasticSearch 信息面板", "panels": [ { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 }, "id": 90, "panels": [], "title": "Cluster", "type": "row" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" }, { "id": 1, "op": "=", "text": "green", "type": 1, "value": "1" }, { "id": 2, "op": "=", "text": "yellow", "type": 1, "value": "2" }, { "id": 3, "op": "=", "text": "red", "type": 1, "value": "3" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 2 }, { "color": "#d44a3a", "value": 3 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 12, "x": 0, "y": 1 }, "id": 92, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "scalar(elasticsearch_cluster_health_status{color=\"green\",env=~\"$env\",cluster=~\"$cluster\"}) + scalar(elasticsearch_cluster_health_status{color=\"yellow\",env=~\"$env\",cluster=~\"$cluster\"}) * 2 + scalar(elasticsearch_cluster_health_status{color=\"red\",env=~\"$env\",cluster=~\"$cluster\"}) * 3", "format": "time_series", "instant": false, "interval": "", "intervalFactor": 1, "legendFormat": "", "refId": "A" } ], "title": "Cluster Status", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 12, "y": 1 }, "id": 8, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "sum(elasticsearch_cluster_health_number_of_nodes{env=~\"$env\",cluster=~\"$cluster\"})/count(elasticsearch_cluster_health_number_of_nodes{env=~\"$env\",cluster=~\"$cluster\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{instance}}", "metric": "elasticsearch_cluster_health_number_of_nodes", "refId": "A", "step": 1800 } ], "timeFrom": null, "timeShift": null, "title": "Running Nodes", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 16, "y": 1 }, "id": 94, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_number_of_data_nodes{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Active Data Nodes", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 0.5 }, { "color": "#d44a3a", "value": 1 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 1 }, "id": 96, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_number_of_pending_tasks{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Pending Tasks", "type": "gauge" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 4 }, "id": 76, "panels": [], "title": "Shards", "type": "row" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 0, "y": 5 }, "id": 78, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_active_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Active Shards", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 4, "y": 5 }, "id": 80, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_active_primary_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Active Primary Shards", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 0.5 }, { "color": "#d44a3a", "value": 1 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 8, "y": 5 }, "id": 82, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_initializing_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Initializing Shards", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 0.5 }, { "color": "#d44a3a", "value": 1 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 12, "y": 5 }, "id": 84, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_relocating_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Relocating Shards", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 0.5 }, { "color": "#d44a3a", "value": 1 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 16, "y": 5 }, "id": 86, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_unassigned_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Unassigned Shards", "type": "gauge" }, { "cacheTimeout": null, "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [ { "id": 0, "op": "=", "text": "N/A", "type": 1, "value": "null" } ], "thresholds": { "mode": "absolute", "steps": [ { "color": "#299c46", "value": null }, { "color": "rgba(237, 129, 40, 0.89)", "value": 0.5 }, { "color": "#d44a3a", "value": 1 } ] }, "unit": "none" }, "overrides": [] }, "gridPos": { "h": 3, "w": 4, "x": 20, "y": 5 }, "id": 88, "interval": null, "links": [], "maxDataPoints": 100, "options": { "orientation": "horizontal", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "elasticsearch_cluster_health_delayed_unassigned_shards{env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{instance}}", "refId": "A" } ], "title": "Delayed Unassigned Shards", "type": "gauge" }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 8 }, "id": 70, "panels": [], "title": "Documents", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 9 }, "hiddenSeries": false, "id": 3, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(elasticsearch_indices_docs{env=~\"$env\",cluster=~\"$cluster\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Documents", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Documents indexed", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 9 }, "hiddenSeries": false, "id": 4, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(elasticsearch_indices_store_size_bytes{env=~\"$env\",cluster=~\"$cluster\"})", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Index Size", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Index Size", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "bytes", "logBase": 1, "max": null, "min": 0, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 0, "y": 18 }, "hiddenSeries": false, "id": 72, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(elasticsearch_indices_indexing_index_total{env=~\"$env\",cluster=~\"$cluster\"}[1h])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{name}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Documents Indexed Rate", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "Documents/s", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 18 }, "hiddenSeries": false, "id": 74, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 1, "links": [], "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(elasticsearch_indices_search_fetch_total{env=~\"$env\",cluster=~\"$cluster\"}[1h])", "format": "time_series", "interval": "", "intervalFactor": 1, "legendFormat": "{{name}}", "refId": "A" } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Query Rate", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "label": "Queris/s", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 27 }, "height": "", "hiddenSeries": false, "id": 64, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(elasticsearch_thread_pool_queue_count{env=~\"$env\",cluster=~\"$cluster\", type!=\"management\"}) by (type)", "interval": "", "intervalFactor": 2, "legendFormat": "Type: {{type}}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Queue Count", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "collapsed": false, "datasource": "Prometheus", "gridPos": { "h": 1, "w": 24, "x": 0, "y": 35 }, "id": 68, "panels": [], "title": "System", "type": "row" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 36 }, "hiddenSeries": false, "id": 65, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "irate(elasticsearch_jvm_gc_collection_seconds_sum{env=~\"$env\",cluster=~\"$cluster\"}[1m])", "interval": "", "intervalFactor": 2, "legendFormat": "{{ name }} {{ gc }}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "GC seconds", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 36 }, "hiddenSeries": false, "id": 66, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "rate(elasticsearch_thread_pool_rejected_count{env=~\"$env\",cluster=~\"$cluster\", type!=\"management\"}[5m])", "interval": "", "intervalFactor": 2, "legendFormat": "{{ name }} {{ type }}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Thread pool rejections", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 44 }, "hiddenSeries": false, "id": 1, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(elasticsearch_thread_pool_active_count{env=~\"$env\",cluster=~\"$cluster\", type!=\"management\"}) by (type)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Type: {{ type }}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Thread Pools", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 44 }, "hiddenSeries": false, "id": 28, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "rightSide": false, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "avg_over_time(elasticsearch_jvm_memory_used_bytes{area=\"heap\",env=~\"$env\",cluster=~\"$cluster\"}[15m]) / elasticsearch_jvm_memory_max_bytes{area=\"heap\",env=~\"$env\",cluster=~\"$cluster\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ name }}", "refId": "A", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "Avg Heap in 15min", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "percentunit", "label": "", "logBase": 1, "max": 1, "min": 0, "show": true }, { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": false } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "editable": true, "error": false, "fieldConfig": { "defaults": { "custom": {}, "links": [] }, "overrides": [] }, "fill": 1, "fillGradient": 0, "grid": {}, "gridPos": { "h": 5, "w": 24, "x": 0, "y": 52 }, "hiddenSeries": false, "id": 5, "legend": { "alignAsTable": true, "avg": true, "current": true, "max": true, "min": true, "show": true, "total": false, "values": true }, "lines": true, "linewidth": 2, "links": [], "nullPointMode": "connected", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 5, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "sum(rate(elasticsearch_transport_rx_packets_total{env=~\"$env\",cluster=~\"$cluster\"}[5m]))", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "RX", "refId": "A", "step": 240 }, { "expr": "sum(rate(elasticsearch_transport_tx_packets_total{env=~\"$env\",cluster=~\"$cluster\"}[5m])) * -1", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "TX", "refId": "B", "step": 240 } ], "thresholds": [], "timeFrom": null, "timeRegions": [], "timeShift": null, "title": "RX/TX Rate 5m", "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "format": "short", "logBase": 1, "max": null, "min": null, "show": true }, { "format": "short", "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(elasticsearch_cluster_health_status,env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values(elasticsearch_cluster_health_status,env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allValue": null, "current": {}, "datasource": "Prometheus", "definition": "label_values(elasticsearch_cluster_health_status{env=\"$env\"},cluster)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "cluster", "multi": false, "name": "cluster", "options": [], "query": { "query": "label_values(elasticsearch_cluster_health_status{env=\"$env\"},cluster)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/grafana_dashboard_json/9-httpd-xin-xi-mian-ban.json ================================================ { "dashboard": { "id": 9, "uid": "TOVJmRPMz", "title": "Httpd 信息面板", "panels": [ { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 0 }, { "color": "green", "value": 1 } ] } }, "overrides": [] }, "gridPos": { "h": 3, "w": 24, "x": 0, "y": 0 }, "id": 5, "links": [], "options": { "colorMode": "background", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "count(apache_up{env=~\"$env\",instance=~\"$host\"} == 1)", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Apache Up", "refId": "A", "step": 120 } ], "timeFrom": null, "timeShift": null, "title": "Apache Up / Down", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 3, "w": 12, "x": 0, "y": 3 }, "id": 7, "links": [], "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_max_fds{env=~\"$env\",instance=~\"$host\",job=\"httpdExporter\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "{{ state }}", "refId": "A", "step": 240 } ], "timeFrom": null, "timeShift": null, "title": "process_max_fds", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 80 } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 3, "w": 12, "x": 12, "y": 3 }, "id": 4, "links": [], "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "auto" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "process_cpu_seconds_total{env=~\"$env\",instance=~\"$host\",job=\"httpdExporter\"}", "format": "time_series", "interval": "", "intervalFactor": 2, "legendFormat": "Load", "refId": "A", "step": 240 } ], "timeFrom": null, "timeShift": null, "title": "process_cpu_seconds_total", "type": "stat" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 0, "y": 6 }, "id": 9, "options": { "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "apache_accesses_total{env=~\"$env\",instance=~\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "apache_accesses_total", "type": "gauge" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] } }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 6, "y": 6 }, "id": 11, "options": { "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "showThresholdLabels": false, "showThresholdMarkers": true, "text": {} }, "pluginVersion": "7.4.3", "targets": [ { "expr": "apache_cpuload{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "apache_cpuload", "type": "gauge" }, { "aliasColors": {}, "breakPoint": "25%", "cacheTimeout": null, "combine": { "label": "Others", "threshold": 0 }, "datasource": "Prometheus", "decimals": -2, "fieldConfig": { "defaults": { "color": {}, "custom": {}, "thresholds": { "mode": "absolute", "steps": [] } }, "overrides": [] }, "fontSize": "70%", "format": "short", "gridPos": { "h": 8, "w": 6, "x": 12, "y": 6 }, "id": 23, "interval": null, "legend": { "header": "", "percentage": false, "show": false, "sideWidth": null, "values": true }, "legendType": "Under graph", "links": [], "nullPointMode": "connected", "pieType": "pie", "pluginVersion": "7.4.3", "strokeWidth": 1, "targets": [ { "expr": "apache_workers{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "title": "apache_workers", "transformations": [], "type": "grafana-piechart-panel", "valueName": "current" }, { "datasource": "Prometheus", "fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "custom": {}, "mappings": [], "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null } ] }, "unit": "s" }, "overrides": [] }, "gridPos": { "h": 8, "w": 6, "x": 18, "y": 6 }, "id": 15, "options": { "colorMode": "value", "graphMode": "none", "justifyMode": "auto", "orientation": "auto", "reduceOptions": { "calcs": [ "lastNotNull" ], "fields": "", "values": false }, "text": {}, "textMode": "value" }, "pluginVersion": "7.4.3", "targets": [ { "expr": "apache_uptime_seconds_total{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "timeFrom": null, "timeShift": null, "title": "apache_uptime_seconds_total", "type": "stat" }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 0, "y": 14 }, "hiddenSeries": false, "id": 19, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_request_size_bytes{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "http_request_size_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:896", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:897", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 12, "x": 12, "y": 14 }, "hiddenSeries": false, "id": 21, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_response_size_bytes{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "http_request_size_bytes", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:985", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:986", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 22 }, "hiddenSeries": false, "id": 13, "legend": { "alignAsTable": false, "avg": false, "current": false, "max": false, "min": false, "rightSide": true, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "apache_scoreboard{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "apache_scoreboard", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:293", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:294", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, "datasource": "Prometheus", "fieldConfig": { "defaults": { "custom": {} }, "overrides": [] }, "fill": 1, "fillGradient": 0, "gridPos": { "h": 8, "w": 24, "x": 0, "y": 30 }, "hiddenSeries": false, "id": 17, "legend": { "avg": false, "current": false, "max": false, "min": false, "show": true, "total": false, "values": false }, "lines": true, "linewidth": 1, "nullPointMode": "null", "options": { "alertThreshold": true }, "percentage": false, "pluginVersion": "7.4.3", "pointradius": 2, "points": false, "renderer": "flot", "seriesOverrides": [], "spaceLength": 10, "stack": false, "steppedLine": false, "targets": [ { "expr": "http_request_duration_microseconds{env=\"$env\",instance=\"$host\"}", "interval": "", "legendFormat": "", "queryType": "randomWalk", "refId": "A" } ], "thresholds": [], "timeRegions": [], "title": "http_request_duration_microseconds", "tooltip": { "shared": true, "sort": 0, "value_type": "individual" }, "type": "graph", "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] }, "yaxes": [ { "$$hashKey": "object:649", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }, { "$$hashKey": "object:650", "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true } ], "yaxis": { "align": false, "alignLevel": null } } ], "templating": { "list": [ { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values({job=\"httpdExporter\"}, env)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "环境", "multi": false, "name": "env", "options": [], "query": { "query": "label_values({job=\"httpdExporter\"}, env)", "refId": "StandardVariableQuery" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values({job=\"httpdExporter\",env=\"$env\"}, instance)", "description": null, "error": null, "hide": 0, "includeAll": false, "label": "host", "multi": false, "name": "host", "options": [], "query": { "query": "label_values({job=\"httpdExporter\",env=\"$env\"}, instance)", "refId": "StandardVariableQuery" }, "refresh": 2, "regex": "", "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false }, { "allFormat": "glob", "allValue": null, "current": { }, "datasource": "Prometheus", "definition": "label_values({job=\"httpdExporter\"}, instance)", "description": null, "error": null, "hide": 2, "includeAll": false, "label": null, "multi": false, "name": "port", "options": [], "query": { "query": "label_values({job=\"httpdExporter\"}, instance)", "refId": "Prometheus-port-Variable-Query" }, "refresh": 1, "regex": "", "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], "tagsQuery": "", "type": "query", "useTags": false } ] }, "version": 1 } } ================================================ FILE: package_hub/openssl_upgrade/upgrade_ssl.sh ================================================ #!/bin/bash #解压新版本的openssl tar -xf '/tmp/upgrade_openssl/openssl-1.0.2k.tar.gz' -C '/usr/local/' openssl_dir="/usr/local/openssl-1.0.2k" openssl_rpm="$(ls ${openssl_dir} | grep -E 'openssl(.*?)\.rpm$')" openssl_rpm_lib="$(ls ${openssl_dir} | grep -E 'openssl-libs-(.*?)\.rpm$')" cd ${openssl_dir} && yum clean all && for rpm in $openssl_rpm;do if [ $rpm != $openssl_rpm_lib ];then openssl_rpm_=$rpm fi done yum localinstall --disablerepo='*' -y $openssl_rpm_ $openssl_rpm_lib &>/dev/null #再次查看openssl版本 openssl version #创建文件作为升级后标志(留痕) echo 'upgrade openssl success' >> '/tmp/upgrade_openssl/is_upgrade_openssl.txt' ================================================ FILE: package_hub/prometheus_rules_template/exporter_status_rule.yml ================================================ groups: - name: Exporter Alert rules: - alert: exporter 异常 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 中的 {{ $labels.app }}_exporter 已经down掉超过一分钟. summary: exporter status(instance {{ $labels.instance }}) expr: exporter_status{env="${ENV}"} == 0 for: 1m labels: severity: critical ================================================ FILE: package_hub/prometheus_rules_template/node_data_rule.yml ================================================ groups: - name: node data disk alert rules: - alert: 主机数据分区磁盘使用率过高 annotations: disk_data_path: ${DISK_DATA_PATH} consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 数据分区使用率当前值为{{ $value | humanize }}%,高于阈值 90% summary: disk_data_used (instance {{ $labels.instance }}) expr: max((node_filesystem_size_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}) *100/(node_filesystem_avail_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}+(node_filesystem_size_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"})))by(instance,env) >= 90 for: 1m labels: job: nodeExporter severity: critical - alert: 主机数据分区磁盘使用率过高 annotations: disk_data_path: ${DISK_DATA_PATH} consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 数据分区使用率当前值为{{ $value | humanize }}%,高于阈值 80% summary: disk_data_used (instance {{ $labels.instance }}) expr: max((node_filesystem_size_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}) *100/(node_filesystem_avail_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}+(node_filesystem_size_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="${DISK_DATA_PATH}"})))by(instance,env) >= 80 for: 1m labels: job: nodeExporter severity: warning ================================================ FILE: package_hub/prometheus_rules_template/node_rule.yml ================================================ groups: - name: node alert rules: - alert: 实例宕机 annotations: consignee: ${EMAIL_ADDRESS} description: 实例 {{ $labels.instance }} monitor_agent进程丢失或主机发生宕机已超过1分钟 summary: 实例宕机({{ $labels.instance }}) expr: sum(up{job="nodeExporter", env="${ENV}"}) by (instance,env) < 1 for: 1m labels: job: nodeExporter severity: critical - alert: 主机CPU使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} CPU使用率当前值为{{ $value | humanize }}%,高于阈值 90% summary: cpu_used (instance {{ $labels.instance }}) expr: (100 - sum(avg without (cpu)(irate(node_cpu_seconds_total{mode='idle', env="${ENV}"}[2m]))) by (instance,env) * 100) >= 90 for: 1m labels: job: nodeExporter severity: critical - alert: 主机CPU使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} CPU使用率当前值为{{ $value | humanize }}%,高于阈值 80% summary: cpu_used (instance {{ $labels.instance }}) expr: (100 - sum(avg without (cpu)(irate(node_cpu_seconds_total{mode='idle', env="${ENV}"}[2m]))) by (instance,env) * 100) >= 80 for: 1m labels: job: nodeExporter severity: warning - alert: 主机内存使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 内存使用率当前值为{{ $value | humanize }}%,高于阈值 90% summary: memory_used (instance {{ $labels.instance }}) expr: (1 - (node_memory_MemAvailable_bytes{env="${ENV}"} / (node_memory_MemTotal_bytes{env="${ENV}"}))) * 100 >= 90 for: 1m labels: job: nodeExporter severity: critical - alert: 主机内存使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 内存使用率当前值为{{ $value | humanize }}%,高于阈值 80% summary: memory_used (instance {{ $labels.instance }}) expr: (1 - (node_memory_MemAvailable_bytes{env="${ENV}"} / (node_memory_MemTotal_bytes{env="${ENV}"}))) * 100 >= 80 for: 1m labels: job: nodeExporter severity: warning - alert: 主机根分区磁盘使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 根分区使用率当前值为{{ $value | humanize }}%,高于阈值 90% summary: disk_root_used (instance {{ $labels.instance }}) expr: max((node_filesystem_size_bytes{env="${ENV}",mountpoint="/"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="/"}) *100/(node_filesystem_avail_bytes{env="${ENV}",mountpoint="/"}+(node_filesystem_size_bytes{env="${ENV}",mountpoint="/"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="/"})))by(instance,env) >= 90 for: 1m labels: job: nodeExporter severity: critical - alert: 主机根分区磁盘使用率过高 annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 根分区使用率当前值为{{ $value | humanize }}%,高于阈值 80% summary: disk_root_used (instance {{ $labels.instance }}) expr: max((node_filesystem_size_bytes{env="${ENV}",mountpoint="/"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="/"}) *100/(node_filesystem_avail_bytes{env="${ENV}",mountpoint="/"}+(node_filesystem_size_bytes{env="${ENV}",mountpoint="/"}-node_filesystem_free_bytes{env="${ENV}",mountpoint="/"})))by(instance,env) >= 80 for: 1m labels: job: nodeExporter severity: warning ================================================ FILE: package_hub/prometheus_rules_template/service_status_rule.yml ================================================ groups: - name: App state rules: - alert: app state annotations: consignee: ${EMAIL_ADDRESS} description: 主机 {{ $labels.instance }} 中的 服务 {{ $labels.app }} 已经down掉超过一分钟. summary: app state(instance {{ $labels.instance }}) expr: probe_success{env="${ENV}"} == 0 for: 1m labels: severity: critical - alert: kafka kafka_consumergroup_lag alert annotations: consignee: 987654321@qq.com description: Kafka 消费组{{ $labels.consumergroup }}消息堆积数过多 {{ humanize $value }} summary: kafka_consumergroup_lag (instance {{ $labels.instance }}) expr: sum(kafka_consumergroup_lag{env="${ENV}"}) by (consumergroup,instance,job,env) > 3000 for: 1m labels: severity: warning - alert: kafka kafka_consumergroup_lag alert annotations: consignee: 987654321@qq.com description: Kafka 消费组{{ $labels.consumergroup }}消息堆积数过多 {{ humanize $value }} summary: kafka_consumergroup_lag (instance {{ $labels.instance }}) expr: sum(kafka_consumergroup_lag{env="${ENV}"}) by (consumergroup,instance,job,env) > 5000 for: 1m labels: severity: critical ================================================ FILE: package_hub/reactor/auth.sls ================================================ {% if 'act' in data and data['act'] == 'denied' %} minion_delete: wheel.key.delete: - args: - match: {{ data['id'] }} {% endif %} ================================================ FILE: package_hub/reactor/start.sls ================================================ agent_start: runner.agent_start.update: - agent_id: {{ data['id'] }} ================================================ FILE: package_hub/reactor/stop.sls ================================================ agent_stop: runner.agent_stop.update: - agent_id_lst: {{ data['present'] }} ================================================ FILE: package_hub/runners/agent_start.py ================================================ # -*- coding: utf-8 -*- # Project: agent_start # Author: jon.liu@yunzhihui.com # Create time: 2021-09-24 11:48 # IDE: PyCharm # Version: 1.0 # Introduction: """ 在agent启动的时候自动获取到Agent的信息,并入库更新处理 """ import os import sys import logging import django CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.join(os.path.dirname( os.path.dirname(CURRENT_DIR)), "omp_server")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "omp_server.settings") django.setup() from db_models.models import Host from utils.plugin.salt_client import SaltClient logger = logging.getLogger("server") def get_agent_detail(target): """ 获取agent详情的方法 :param target: agent的id :return: """ try: salt_obj = SaltClient() _flag, res = salt_obj.fun(target=target, fun="saltutil.sync_modules") logger.info(f"同步模块返回标志: {_flag}; 返回值: {target} {res}") for i in range(5): _flag, res = salt_obj.fun(target, "get_agent_info.get_agent_info") if _flag: break if not _flag: logger.error(f"获取{target} get_agent_info详情失败: {res}") return logger.info(f"获取{target} get_agent_info详情成功: {res}") obj_list = Host.objects.filter(ip=target) if obj_list: obj_list.update( memory=res.get("memory", {}).get("memory_total", 0), cpu=res.get("cpu", 0), disk=res.get("disk", {}), host_agent=0, host_name=res.get("hostname") ) logger.info(f"更新{target}状态成功!") # TODO 暂时屏蔽自动入库逻辑,待设计完善后再进行补充 # else: # Host( # ip=target, # memory=res.get("memory", {}).get("memory_total", 0), # cpu=res.get("cpu", 0), # disk=res.get("disk", {}), # host_agent=0, # host_name=res.get("hostname") # ).save() # logger.info(f"插入{target}状态成功!") except Exception as e: logger.error(f"{target}状态更新失败: {str(e)}") def update(agent_id): """ 更新agent的代码 :param agent_id: :return: """ get_agent_detail(target=agent_id) ================================================ FILE: package_hub/runners/agent_stop.py ================================================ # -*- coding: utf-8 -*- # Project: agent_stop # Author: jon.liu@yunzhihui.com # Create time: 2021-09-24 11:48 # IDE: PyCharm # Version: 1.0 # Introduction: """ 在agent启动的时候自动获取到Agent的信息,并入库更新处理 """ import os import sys import logging import django CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.append(os.path.join(os.path.dirname( os.path.dirname(CURRENT_DIR)), "omp_server")) os.environ.setdefault("DJANGO_SETTINGS_MODULE", "omp_server.settings") django.setup() from db_models.models import Host logger = logging.getLogger("server") def update_agent_detail(agent_id_lst): """ 获取agent详情的方法 :param agent_id_lst: agent的id lst :return: """ try: if not isinstance(agent_id_lst, list): return # 数据库中的agent是正常的,如果其id不再present列表内,则将其状态更新为'2-启动失败' # 如果当前在线id列表有值,那么排除此部分值后刷新Agent状态 # 如果当前在线id列表无值,说明所有 原正常的Agent 均处于离线状态 if len(agent_id_lst) != 0: Host.objects.filter( host_agent=0).exclude( ip__in=agent_id_lst).all().update(host_agent=2) else: Host.objects.filter(host_agent=0).update(host_agent=2) Host.objects.filter( ip__in=agent_id_lst).all().update(host_agent=0) logger.info(f"更新当前在线Agent: {agent_id_lst}状态更新成功!") except Exception as e: logger.error(f"{agent_id_lst}状态更新失败: {str(e)}") def update(agent_id_lst): """ 更新agent的代码 :param agent_id_lst: :return: """ update_agent_detail(agent_id_lst=agent_id_lst) ================================================ FILE: package_hub/template/app_publish_readme.md ================================================ # OMP 社区版-应用商店发布说明文档 [TOC] ## 1. 说明 用户可以在应用商店发布“基础组件”与“应用服务”两个维度的产品,在区分上,应用服务可以理解为完整的提供某一类服务的产品,产品内部可由一个或多个“服务”组成 ,比如gitlab、jenkins等。基础组件的角色更多是作为其他完成产品的一部分存在,以完成产品的某些功能需求,如mysql、redis等。 ## 2. 基础组件打包规范 注:用户在发布基础组件安装包时,需按照以下规范打包上传才可以正常发布 ### 2.1. 目录规范 以MySQL服务为例,需将涉及到的文件统一放在 mysql目录下,目录名称与该服务名称保持一致,目录中需要提供与该目录名称一致的配置文件(如:mysql.yaml)、产品图标(如:mysql.svg) 和其他所需文件(如安装脚本等) **示例:** ```shell $ tree ./mysql -L 2 ./mysql # 目录名称,请与组件名称一致 ├── mysql.svg # 平台展示组件图标,请使用 “组件名称.svg ” 命名,与目录名称保持一致 ├── mysql.yaml # 组件配置文件, 记录该组件安装、升级等所需信息, 请使用 “组件名称.yaml” 命名,与目录名称保持一致 └── scripts # 组件的安装、启动等控制脚本,该脚本在安装时会调用 │   ├── init.py # 初始化脚本 │   ├── install.py # 组件安装脚本 │   ├── mysql # 组件启动、停止控制脚本,建议与服务名称一致 │   ├── mysql_backup.py # 其他动作脚本,如备份等 ``` **备注:** 1. 组件图标请使用svg格式图片,如不添加会显示平台缺省图标; 2. 确保目录名称(mysql)、配置文件(mysql.yaml) 、图标(mysql.svg) 名称统一, 上传安装包时,平台将根据名称校验对应文件合法性,如名称不一致,可能会导致校验不通过等问题; 3. 确保安装包解压后是一个整体目录 ### 2.2. 压缩包命名规范 请使用 `{name}-{version}-{others}-{package_md5}.tar.gz` 格式进行打包命名 1. name: 安装包名称,建议字符: `英文` `数字` `_` 2. version: 安装包版本,建议字符: `英文` `数字` `_` `.` 3. others: 其他信息,建议字符: `英文` `数字` `_` `.` 4. package_md5: 安装包MD5 值 例如:`mysql-5.7.31-beta-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz` ```shell $ tar czf mysql-5.7.31.tar.gz mysql $ md5sum mysql-5.7.31.tar.gz 8e955b24fefe7061eb79cfc61a9a02a1 $ mv mysql-5.7.31.tar.gz mysql-5.7.31-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz ``` ### 2.3. 配置文件(yaml)说明 平台预留KEY值(该KEY值存在指定定义,请准确使用): | KEY | 说明 | 备注 | | ------------ | ------------ | -------------------------------------- | | service_port | 服务端口 | 供其他程序连接的端口号 | | base_dir | 应用安装目录 | | | log_dir | 应用日志目录 | 服务的日志采集会采集该目录下*.log 文件 | | data_dir | 应用数据目录 | | | username | 用户名 | | | password | 密码 | | ```yaml # 类型定义,发布基础组件时 ,指定类型为 component (类型:string) kind: component # 组件在平台显示的名称,请与组件目录名称保持一致,建议字符:英文、数字、_ (类型:string) name: mysql # 上传后显示的组件版本,建议字符: 数字、字母、_ 、. (类型:string) version: 5.7.31 # 组件描述信息,建议长度256字符之内,请针对组件书写贴切的描述文字 (类型:string) description: "MySQL是一个关系型数据库管理系统,由瑞典MySQL AB 公司开发,属于 Oracle 旗下产品。MySQL 是最流行的关系型数据库管理系统之一,在 WEB 应用方面,MySQL是最好的 RDBMS (Relational Database Management System,关系数据库管理系统) 应用软件之一。" # 组件所属标签,请针对组件功能设置准确标签,平台会针对该标签对组件进行分类,(类型:list[string,string...]) labels: - 数据库 # 指定该服务安装后是否需要启动 (类型:boolean) auto_launch: false # 指定组件是否为基础环境组件,如 jdk, 该类组件以基础环境方式安装 (类型:boolean) base_env: flase # 定义组件所需端口号,如不启用端口,可留空 (类型:list[map,map...]) ports: # 端口描述名称,用户在安装时会以该名称显示表单内容(类型:string) - name: 服务端口 # 端口协议,支持 TCP/UDP protocol: TCP # 端口英文描述名称,该key会传入到安装脚本中 (类型: string)支持(英文、数字、_) key: service_port # 注:service_port 为保留关键词,表示 为 提供服务的端口 # 组件的默认端口号,在安装时,会以该值填入表单中(类型: int) default: 3306 # 组件监控相关配置,定义该组件在安装后如何监控 ,如果不需要监控可留空 (类型: map) monitor: # 监控进程名称,如“mysqld”,平台在发现mysqld进程不存在后,会发送告警提醒 ,不需要监控可留空(类型:string) process_name: "mysqld" # 监控端口号,请根据 ports 中的变量设置,不需要监控可留空 (类型: {string}) metric_port: {service_port} --- # 设置集群模式方式,如果组件需要支持多种方式安装,可以在该字段中定义,如只支持单个实例安装,可留空(类型:map[list[map,map...]]) deploy: # 定义单实例模式安装 (类型:list[map,map...]) single: # 部署方式的中文描述名称,该值会在表单中选择集群模式时显示 (类型:string) - name: 单实例 # 该模式的key值 (类型:string) key: single # 定义多种集群模式安装 (类型:list[map,map...]) complex: # 部署方式的中文描述名称,该值会在表单中选择集群模式时显示 (类型:string) - name: 主从模式 # 该模式的key值 (类型:string) key: master_slave # 集群节点设置 (类型: map) nodes: # 初始节点数量 (类型:int) start: 2 # 增加节点步长 (类型:int) step: 1 # 定义该组件安装所需依赖组件名称与版本,如不需其他组件依赖,可留空 (类型: list[map,map..]) #例: #dependencies: # - name: jdk # version: 8u223 dependencies: # 该组件所需最小资源需求 (类型:map) resources: # cpu最小需求 ,1000m 表示 1核 (类型:string) cpu: 1000m # 内存最小需求, 500m 表示 500兆内存 (类型:string) memory: 500m --- # 定义安装组件时所需参数,该参数会传入到 安装脚本中 (类型:list[map,map...]) install: # 传入参数中文描述名称,该名称会在用户安装组件时显示到表单中 (类型: string) - name: "安装目录" # 传入参数key值,会将该key与值 传入到安装脚本中 (类型:string) key: base_dir # 上面key默认值 (类型: stirng) default: "{data_path}/mysql" # 注: {data_path} 为主机数据目录占位符,请勿使用其他代替 - name: "数据目录" key: data_dir default: "{data_path}/mysql/data" - name: "日志目录" key: log_dir default: "{data_path}/mysql/log" - name: "用户名" key: username default: root - name: "密码" key: password default: "123456" # 程序控制脚本与服务目录的相对路径 (类型:map) control: # 启动脚本路径,如没有可留空 (类型:string) start: "./scripts/mysql start" # 停止脚本路径,如没有可留空 (类型:stirng) stop: "./scripts/mysql stop" # 重启脚本路径,如没有可留空 (类型:stirng) restart: "./scripts/mysql restart" # 重载脚本路径,如没有可留空 (类型:stirng) reload: # 安装脚本路径,必填 (类型:stirng) install: "./scripts/install.py" # 初始化脚本路径,必填 (类型:stirng) init: "./scripts/init.py" ``` ### 2.4. 安装脚本编写说明 在安装包成功发布后,可通过平台进行安装,平台会调用配置文件中指定的安装脚本进行程序安装,平台将会把安装脚本所需参数以如下形式进行传参,需要脚本在编写时对此进行支持。 传参示例: ```shell $ python ./scripts/install.py --local_ip 192.168.1.2 --data_json /data/LKJD82JDL.json ``` 其中 local_ip 为安装主机的IP地址,data_json为安装所需数据文件路径 安装脚本需要根据data_json内数据进行组件的安装、替换其他文件内的占位符 data.json示例: ```json [ { "name":"nacos", "ip":"1.1.1.1", "version":"2.0.1", "ports":[ { "key":"service_port", "name":"xxx端口", "default":8001 } ], "install_arg":[ { "key":"base_dir", "name":"服务目录", "default":"/data/app/nacos" }, { "key":"data_dir", "name":"数据目录", "default":"/data/appData/nacos" }, { "key":"username", "name":"用户名", "default":"admin" }, { "key":"password", "name":"密码", "default":"admin123" } ], "deploy_mode":{ }, "cluster_name":"", "instance_name":"nacos-1", "dependence":[ { "name":"mysql", "instance_name":"mysql-100", "cluster_name":"mysql-JDLK3KA" } ] }, { "name":"mysql", "ip":"192.1.2.3", "version":"5.0.1", "ports":[ { "key":"service_port", "name":"服务端口", "default":10601 } ], "install_arg":[ { "key":"base_dir", "name":"服务目录", "default":"/data/app/mysql" }, { "key":"data_dir", "name":"数据目录", "default":"/data/appData/mysql" }, { "key":"data_dir", "name":"日志目录", "default":"/data/appData/log" }, { "key":"username", "name":"用户名", "default":"root" }, { "key":"password", "name":"密码", "default":"root123" } ], "deploy_mode":{ }, "cluster_name":"", "instance_name":"mysql-100", "dependence":[ ] } ] ``` ## 3. 应用服务打包规范 ### 3.1. 目录规范 在发布类别为应用服务的产品时,需要将产品名称、所属产品的服务名称、版本号做到全局统一 **目录示例:** 发布产品名称为“omp",其中包含 3个服务为“omp_server","omp_web","omp_component" 的目录结构如下 ```shell $ tree omp omp ├── omp.svg # 定义产品图标,会在平台中展示,如果不创建则平台会展示缺省图标 ├── omp # 定义产品下服务配置文件目录,将所需服务的配置文件存在该目录 │ ├── omp_server.yaml # 服务 omp_server 配置文件,文件名需要与服务名称一致 │ ├── omp_web.yaml # 服务 omp_web 配置文件,文件名需要与服务名称一致 │ └── omp_component.yaml # 服务 omp_agent 配置文件,文件名需要与服务名称一致 ├── omp_server-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_server 压缩包,以“-” 为分隔符,第一个为服务名称,需要与服务名称一致,格式为 {service_name}-{service_version}-{others}-{package_md5}.tar.gz ├── omp_web-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_web 压缩包 ├── omp_component-0.1.0-5d1ac8ce87323fc399506d1335ae5c98.tar.gz # 服务 omp_agent 压缩包 └── omp.yaml # 定义产品配置文件,文件名需要与产品名称一致 ``` 其中服务目录以omp_server为例: ```shell $ tree omp_server omp_server # 服务包解压后目录名称,与服务名一致 ├── bin # 服务控制脚本目录,启动、停止等 │ └── omp_server # 服务控制脚本,与服务名称一致 ├── omp_server.yaml # 服务配置文件,与产品包中保持一致 └── scripts # 安装、升级脚本目录 ├── init.py # 初始化脚本 ├── install.py # 安装脚本 └── update.py # 升级脚本 ``` ### 3.2. 压缩包命名规范 请使用 `{name}-{version}-{others}-{package_md5}.tar.gz` 格式进行打包命名 1. name: 安装包名称,建议字符: `英文` `数字` `_` 2. version: 安装包版本,建议字符: `英文` `数字` `_` `.` 3. others: 其他信息,建议字符: `英文` `数字` `_` `.` 4. package_md5: 安装包MD5 值 例如: omp-0.1.0-8e955b24fefe7061eb79cfc61a9a02a1.tar.gz ### 3.3. 配置文件yaml说明 发布类别为应用服务的产品时,需分别对 产品配置文件和产品下服务配置文件进行配置 #### 3.3.1. 产品配置文件(yaml)格式说明 ```yaml # 类型定义,发布应用服务时,产品指定类型为 product (类型:string) kind: product # 定义产品名称,此名称需要与产品目录名称、产品配置文件名称保持一致,建议字符:英文、数字、_ (类型: string) name: omp # 上传后显示的产品版本,建议字符: 数字、字母、_ 、. (类型:string) version: # 产品描述信息,建议长度256字符之内,请针对产品书写贴切的描述文字 (类型:string) description: "运维管理平台(OperationManagementPlatform,以下简称OMP)以管理服务为中心,为服务的安装、管理提供便捷可靠的方式。" # 组件所属标签,请针对组件功能设置准确标签,平台会针对该标签对组件进行分类,(类型:list[string,string...]) labels: - omp # 定义该产品安装所需依赖产品名称与版本,如不需其他产品依赖,可留空 (类型: list[map,map..]) dependencies: # 定义该产品下包含的服务信息,请确保列表中的服务包都包含在目录中,并且名称保持一致 (类型: list[map,map...]) service: # 包含服务名称,请与服务包名保持一致 (类型: string) - name: omp_server # 服务版本,请与服务包版本一致 (类型:string) version: 0.1.0 - name: omp_web version: 0.1.0 - name: omp_component version: 0.1.0 ``` #### 3.3.2. 服务配置文件(yaml)格式说明 ```yaml # 类型定义,发布应用服务时,产品包含的服务指定类型为 service (类型:string) kind: service # 服务在平台显示的名称,请与服务目录名称保持一致,建议字符:英文、数字、_ (类型:string) name: omp_server # 上传后显示的服务版本,建议字符: 数字、字母、_ 、. (类型:string) version: 0.1.0 # 服务描述信息,建议长度256字符之内,请针对组件书写贴切的描述文字 (类型:string) description: "服务描述内容..." # 指定该服务安装后是否需要启动 (类型:boolean) auto_launch: true # 指定服务是否为基础环境组件,如 jdk, 该类组件以基础环境方式安装 (类型:boolean) base_env: flase # 定义服务所需端口号,如不启用端口,可留空 (类型:list[map,map...]) ports: # 端口描述名称,用户在安装时会以该名称显示表单内容(类型:string) - name: 服务端口 # 端口协议,支持 TCP/UDP protocol: TCP # 端口英文描述名称,该key会传入到安装脚本中 (类型: string)支持(英文、数字、_) key: service_port # 注:service_port 为保留关键词,表示 为 提供服务的端口 # 组件的默认端口号,在安装时,会以该值填入表单中(类型: int) default: 19001 # 服务监控相关配置,定义该服务在安装后如何监控 ,如果不需要监控可留空 (类型: map) monitor: # 监控进程名称,如“service_a”,平台在发现service_a进程不存在后,会发送告警提醒,不需要监控可留空(类型:string) process_name: "" # 监控端口号,请根据 ports 中的变量设置,不需要监控可留空 (类型: {string}) metric_port: {service_port} --- # 定义该组件安装所需依赖组件名称与版本,如不需其他组件依赖,可留空 (类型: list[map,map..]) dependencies: - name: mysql version: 5.7.31 - name: redis version: 5.0.1 - name: python version: 3.8.3 # 该组件所需最小资源需求 (类型:map) resources: # cpu最小需求 ,1000m 表示 1核 (类型:string) cpu: 1000m # 内存最小需求, 500m 表示 500兆内存 (类型:string) memory: 500m --- # 定义安装组件时所需参数,该参数会传入到 安装脚本中 (类型:list[map,map...]) install: # 传入参数中文描述名称,该名称会在用户安装组件时显示到表单中 (类型: string) - name: "安装目录" # 传入参数key值,会将该key与值 传入到安装脚本中 (类型:string) key: base_dir # 上面key默认值 (类型: stirng) default: "{data_path}/omp_server" # 注: {data_path} 为主机数据目录占位符,请勿使用其他代替 # - name: "JVM设置" # key: jvm # default: "-XX:MaxPermSize=512m -Djava.awt.headless=true" # 程序控制脚本与服务目录的相对路径 (类型:map) control: # 启动脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:string) start: "./bin/omp_server start" # 停止脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:stirng) stop: "./bin/omp_server stop" # 重启脚本路径,如没有可留空,脚本名称建议与服务名称一致 (类型:stirng) restart: "./bin/omp_server restart" # 重载脚本路径,如没有可留空 (类型:stirng) reload: # 安装脚本路径,必填 (类型:stirng) install: "./scripts/install.py" # 初始化脚本路径,必填 (类型:stirng) init: "./scripts/init.py" ``` ================================================ FILE: package_hub/template/inspection_html/asset-manifest.json ================================================ { "files": { "main.css": "/static/css/main.041ca26a.chunk.css", "main.js": "/static/js/main.e4ade54a.chunk.js", "main.js.map": "/static/js/main.e4ade54a.chunk.js.map", "runtime-main.js": "/static/js/runtime-main.da7bcbe2.js", "runtime-main.js.map": "/static/js/runtime-main.da7bcbe2.js.map", "static/css/2.8ca66de9.chunk.css": "/static/css/2.8ca66de9.chunk.css", "static/js/2.0ca9bd94.chunk.js": "/static/js/2.0ca9bd94.chunk.js", "static/js/2.0ca9bd94.chunk.js.map": "/static/js/2.0ca9bd94.chunk.js.map", "index.html": "/index.html", "static/css/2.8ca66de9.chunk.css.map": "/static/css/2.8ca66de9.chunk.css.map", "static/css/main.041ca26a.chunk.css.map": "/static/css/main.041ca26a.chunk.css.map", "static/js/2.0ca9bd94.chunk.js.LICENSE.txt": "/static/js/2.0ca9bd94.chunk.js.LICENSE.txt", "static/media/index.02867153.less": "/static/media/index.02867153.less", "static/media/index.2041a1d4.less": "/static/media/index.2041a1d4.less", "static/media/index.2f186d27.less": "/static/media/index.2f186d27.less", "static/media/index.32dc937e.less": "/static/media/index.32dc937e.less", "static/media/index.383af9c4.less": "/static/media/index.383af9c4.less", "static/media/index.51825487.less": "/static/media/index.51825487.less", "static/media/index.60c6e3ea.less": "/static/media/index.60c6e3ea.less", "static/media/index.67101e84.less": "/static/media/index.67101e84.less", "static/media/index.68b48da1.less": "/static/media/index.68b48da1.less", "static/media/index.73987a8f.less": "/static/media/index.73987a8f.less", "static/media/index.8372475c.less": "/static/media/index.8372475c.less", "static/media/index.85c775e4.less": "/static/media/index.85c775e4.less", "static/media/index.8c12967b.less": "/static/media/index.8c12967b.less", "static/media/index.976fe83e.less": "/static/media/index.976fe83e.less", "static/media/index.cae8fdaf.less": "/static/media/index.cae8fdaf.less", "static/media/index.d15ddbc9.less": "/static/media/index.d15ddbc9.less", "static/media/index.d61ddb9a.less": "/static/media/index.d61ddb9a.less", "static/media/index.e1e14bcc.less": "/static/media/index.e1e14bcc.less", "static/media/index.less": "/static/media/index.e90871b5.less", "static/media/index.module.b57695f6.less": "/static/media/index.module.b57695f6.less" }, "entrypoints": [ "static/js/runtime-main.da7bcbe2.js", "static/css/2.8ca66de9.chunk.css", "static/js/2.0ca9bd94.chunk.js", "static/css/main.041ca26a.chunk.css", "static/js/main.e4ade54a.chunk.js" ] } ================================================ FILE: package_hub/template/inspection_html/index.html ================================================ 云智慧
================================================ FILE: package_hub/template/inspection_html/static/css/2.8ca66de9.chunk.css ================================================ /*! * * antd v3.26.18 * * Copyright 2015-present, Alipay, Inc. * All rights reserved. * */body,html{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,:after,:before{-webkit-box-sizing:border-box;box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;color:rgba(0,0,0,.65);font-size:14px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";font-variant:tabular-nums;line-height:1.5;background-color:#fff;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}[tabindex="-1"]:focus{outline:none!important}hr{-webkit-box-sizing:content-box;box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[data-original-title],abbr[title]{text-decoration:underline;-webkit-text-decoration:underline dotted;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=number],input[type=password],input[type=text],textarea{-webkit-appearance:none}dl,ol,ul{margin-top:0;margin-bottom:1em}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1890ff;text-decoration:none;background-color:transparent;outline:none;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;-webkit-text-decoration-skip:objects}a:hover{color:#40a9ff}a:active{color:#096dd9}a:active,a:hover{text-decoration:none;outline:0}a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}code,kbd,pre,samp{font-size:1em;font-family:"SFMono-Regular",Consolas,"Liberation Mono",Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}svg:not(:root){overflow:hidden}[role=button],a,area,button,input:not([type=range]),label,select,summary,textarea{-ms-touch-action:manipulation;touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;color:rgba(0,0,0,.45);text-align:left;caption-side:bottom}th{text-align:inherit}button,input,optgroup,select,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[type=reset],[type=submit],button,html [type=button]{-webkit-appearance:button}[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner,button::-moz-focus-inner{padding:0;border-style:none}input[type=checkbox],input[type=radio]{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0}input[type=date],input[type=datetime-local],input[type=month],input[type=time]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}::-moz-selection{color:#fff;background:#1890ff}::selection{color:#fff;background:#1890ff}.clearfix{zoom:1}.clearfix:after,.clearfix:before{display:table;content:""}.clearfix:after{clear:both}.anticon{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.anticon>*{line-height:1}.anticon svg{display:inline-block}.anticon:before{display:none}.anticon .anticon-icon{display:block}.anticon[tabindex]{cursor:pointer}.anticon-spin,.anticon-spin:before{display:inline-block;-webkit-animation:loadingCircle 1s linear infinite;animation:loadingCircle 1s linear infinite}.fade-appear,.fade-enter,.fade-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.fade-appear.fade-appear-active,.fade-enter.fade-enter-active{-webkit-animation-name:antFadeIn;animation-name:antFadeIn;-webkit-animation-play-state:running;animation-play-state:running}.fade-leave.fade-leave-active{-webkit-animation-name:antFadeOut;animation-name:antFadeOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.fade-appear,.fade-enter{opacity:0}.fade-appear,.fade-enter,.fade-leave{-webkit-animation-timing-function:linear;animation-timing-function:linear}@-webkit-keyframes antFadeIn{0%{opacity:0}to{opacity:1}}@keyframes antFadeIn{0%{opacity:0}to{opacity:1}}@-webkit-keyframes antFadeOut{0%{opacity:1}to{opacity:0}}@keyframes antFadeOut{0%{opacity:1}to{opacity:0}}.move-up-appear,.move-up-enter,.move-up-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-up-appear.move-up-appear-active,.move-up-enter.move-up-enter-active{-webkit-animation-name:antMoveUpIn;animation-name:antMoveUpIn;-webkit-animation-play-state:running;animation-play-state:running}.move-up-leave.move-up-leave-active{-webkit-animation-name:antMoveUpOut;animation-name:antMoveUpOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.move-up-appear,.move-up-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-up-leave{-webkit-animation-timing-function:cubic-bezier(.6,.04,.98,.34);animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-down-appear,.move-down-enter,.move-down-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-down-appear.move-down-appear-active,.move-down-enter.move-down-enter-active{-webkit-animation-name:antMoveDownIn;animation-name:antMoveDownIn;-webkit-animation-play-state:running;animation-play-state:running}.move-down-leave.move-down-leave-active{-webkit-animation-name:antMoveDownOut;animation-name:antMoveDownOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.move-down-appear,.move-down-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-down-leave{-webkit-animation-timing-function:cubic-bezier(.6,.04,.98,.34);animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-left-appear,.move-left-enter,.move-left-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-left-appear.move-left-appear-active,.move-left-enter.move-left-enter-active{-webkit-animation-name:antMoveLeftIn;animation-name:antMoveLeftIn;-webkit-animation-play-state:running;animation-play-state:running}.move-left-leave.move-left-leave-active{-webkit-animation-name:antMoveLeftOut;animation-name:antMoveLeftOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.move-left-appear,.move-left-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-left-leave{-webkit-animation-timing-function:cubic-bezier(.6,.04,.98,.34);animation-timing-function:cubic-bezier(.6,.04,.98,.34)}.move-right-appear,.move-right-enter,.move-right-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.move-right-appear.move-right-appear-active,.move-right-enter.move-right-enter-active{-webkit-animation-name:antMoveRightIn;animation-name:antMoveRightIn;-webkit-animation-play-state:running;animation-play-state:running}.move-right-leave.move-right-leave-active{-webkit-animation-name:antMoveRightOut;animation-name:antMoveRightOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.move-right-appear,.move-right-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.move-right-leave{-webkit-animation-timing-function:cubic-bezier(.6,.04,.98,.34);animation-timing-function:cubic-bezier(.6,.04,.98,.34)}@-webkit-keyframes antMoveDownIn{0%{-webkit-transform:translateY(100%);transform:translateY(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antMoveDownIn{0%{-webkit-transform:translateY(100%);transform:translateY(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antMoveDownOut{0%{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateY(100%);transform:translateY(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antMoveDownOut{0%{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateY(100%);transform:translateY(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes antMoveLeftIn{0%{-webkit-transform:translateX(-100%);transform:translateX(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antMoveLeftIn{0%{-webkit-transform:translateX(-100%);transform:translateX(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antMoveLeftOut{0%{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateX(-100%);transform:translateX(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antMoveLeftOut{0%{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateX(-100%);transform:translateX(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes antMoveRightIn{0%{-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antMoveRightIn{0%{-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antMoveRightOut{0%{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antMoveRightOut{0%{-webkit-transform:translateX(0);transform:translateX(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateX(100%);transform:translateX(100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes antMoveUpIn{0%{-webkit-transform:translateY(-100%);transform:translateY(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antMoveUpIn{0%{-webkit-transform:translateY(-100%);transform:translateY(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antMoveUpOut{0%{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateY(-100%);transform:translateY(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antMoveUpOut{0%{-webkit-transform:translateY(0);transform:translateY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:translateY(-100%);transform:translateY(-100%);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes loadingCircle{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes loadingCircle{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}[ant-click-animating-without-extra-node=true],[ant-click-animating=true]{position:relative}html{--antd-wave-shadow-color:#1890ff}.ant-click-animating-node,[ant-click-animating-without-extra-node=true]:after{position:absolute;top:0;right:0;bottom:0;left:0;display:block;border-radius:inherit;-webkit-box-shadow:0 0 0 0 #1890ff;-webkit-box-shadow:0 0 0 0 var(--antd-wave-shadow-color);box-shadow:0 0 0 0 #1890ff;box-shadow:0 0 0 0 var(--antd-wave-shadow-color);opacity:.2;-webkit-animation:fadeEffect 2s cubic-bezier(.08,.82,.17,1),waveEffect .4s cubic-bezier(.08,.82,.17,1);animation:fadeEffect 2s cubic-bezier(.08,.82,.17,1),waveEffect .4s cubic-bezier(.08,.82,.17,1);-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;content:"";pointer-events:none}@-webkit-keyframes waveEffect{to{-webkit-box-shadow:0 0 0 #1890ff;box-shadow:0 0 0 #1890ff;-webkit-box-shadow:0 0 0 6px #1890ff;-webkit-box-shadow:0 0 0 6px var(--antd-wave-shadow-color);box-shadow:0 0 0 6px #1890ff;box-shadow:0 0 0 6px var(--antd-wave-shadow-color)}}@keyframes waveEffect{to{-webkit-box-shadow:0 0 0 #1890ff;box-shadow:0 0 0 #1890ff;-webkit-box-shadow:0 0 0 6px #1890ff;-webkit-box-shadow:0 0 0 6px var(--antd-wave-shadow-color);box-shadow:0 0 0 6px #1890ff;box-shadow:0 0 0 6px var(--antd-wave-shadow-color)}}@-webkit-keyframes fadeEffect{to{opacity:0}}@keyframes fadeEffect{to{opacity:0}}.slide-up-appear,.slide-up-enter,.slide-up-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-up-appear.slide-up-appear-active,.slide-up-enter.slide-up-enter-active{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-up-leave.slide-up-leave-active{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.slide-up-appear,.slide-up-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.23,1,.32,1);animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-up-leave{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-down-appear,.slide-down-enter,.slide-down-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-down-appear.slide-down-appear-active,.slide-down-enter.slide-down-enter-active{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-down-leave.slide-down-leave-active{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.slide-down-appear,.slide-down-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.23,1,.32,1);animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-down-leave{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-left-appear,.slide-left-enter,.slide-left-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-left-appear.slide-left-appear-active,.slide-left-enter.slide-left-enter-active{-webkit-animation-name:antSlideLeftIn;animation-name:antSlideLeftIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-left-leave.slide-left-leave-active{-webkit-animation-name:antSlideLeftOut;animation-name:antSlideLeftOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.slide-left-appear,.slide-left-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.23,1,.32,1);animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-left-leave{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}.slide-right-appear,.slide-right-enter,.slide-right-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.slide-right-appear.slide-right-appear-active,.slide-right-enter.slide-right-enter-active{-webkit-animation-name:antSlideRightIn;animation-name:antSlideRightIn;-webkit-animation-play-state:running;animation-play-state:running}.slide-right-leave.slide-right-leave-active{-webkit-animation-name:antSlideRightOut;animation-name:antSlideRightOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.slide-right-appear,.slide-right-enter{opacity:0;-webkit-animation-timing-function:cubic-bezier(.23,1,.32,1);animation-timing-function:cubic-bezier(.23,1,.32,1)}.slide-right-leave{-webkit-animation-timing-function:cubic-bezier(.755,.05,.855,.06);animation-timing-function:cubic-bezier(.755,.05,.855,.06)}@-webkit-keyframes antSlideUpIn{0%{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antSlideUpIn{0%{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antSlideUpOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antSlideUpOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes antSlideDownIn{0%{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:1}}@keyframes antSlideDownIn{0%{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:1}}@-webkit-keyframes antSlideDownOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:1}to{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:0}}@keyframes antSlideDownOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:1}to{-webkit-transform:scaleY(.8);transform:scaleY(.8);-webkit-transform-origin:100% 100%;transform-origin:100% 100%;opacity:0}}@-webkit-keyframes antSlideLeftIn{0%{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antSlideLeftIn{0%{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antSlideLeftOut{0%{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antSlideLeftOut{0%{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@-webkit-keyframes antSlideRightIn{0%{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:0}to{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:1}}@keyframes antSlideRightIn{0%{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:0}to{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:1}}@-webkit-keyframes antSlideRightOut{0%{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:1}to{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:0}}@keyframes antSlideRightOut{0%{-webkit-transform:scaleX(1);transform:scaleX(1);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:1}to{-webkit-transform:scaleX(.8);transform:scaleX(.8);-webkit-transform-origin:100% 0;transform-origin:100% 0;opacity:0}}.swing-appear,.swing-enter{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.swing-appear.swing-appear-active,.swing-enter.swing-enter-active{-webkit-animation-name:antSwingIn;animation-name:antSwingIn;-webkit-animation-play-state:running;animation-play-state:running}@-webkit-keyframes antSwingIn{0%,to{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%{-webkit-transform:translateX(10px);transform:translateX(10px)}60%{-webkit-transform:translateX(-5px);transform:translateX(-5px)}80%{-webkit-transform:translateX(5px);transform:translateX(5px)}}@keyframes antSwingIn{0%,to{-webkit-transform:translateX(0);transform:translateX(0)}20%{-webkit-transform:translateX(-10px);transform:translateX(-10px)}40%{-webkit-transform:translateX(10px);transform:translateX(10px)}60%{-webkit-transform:translateX(-5px);transform:translateX(-5px)}80%{-webkit-transform:translateX(5px);transform:translateX(5px)}}.zoom-appear,.zoom-enter,.zoom-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-appear.zoom-appear-active,.zoom-enter.zoom-enter-active{-webkit-animation-name:antZoomIn;animation-name:antZoomIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-leave.zoom-leave-active{-webkit-animation-name:antZoomOut;animation-name:antZoomOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-appear,.zoom-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-big-appear,.zoom-big-enter,.zoom-big-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-big-appear.zoom-big-appear-active,.zoom-big-enter.zoom-big-enter-active{-webkit-animation-name:antZoomBigIn;animation-name:antZoomBigIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-big-leave.zoom-big-leave-active{-webkit-animation-name:antZoomBigOut;animation-name:antZoomBigOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-big-appear,.zoom-big-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-big-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-big-fast-appear,.zoom-big-fast-enter,.zoom-big-fast-leave{-webkit-animation-duration:.1s;animation-duration:.1s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-big-fast-appear.zoom-big-fast-appear-active,.zoom-big-fast-enter.zoom-big-fast-enter-active{-webkit-animation-name:antZoomBigIn;animation-name:antZoomBigIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-big-fast-leave.zoom-big-fast-leave-active{-webkit-animation-name:antZoomBigOut;animation-name:antZoomBigOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-big-fast-appear,.zoom-big-fast-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-big-fast-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-up-appear,.zoom-up-enter,.zoom-up-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-up-appear.zoom-up-appear-active,.zoom-up-enter.zoom-up-enter-active{-webkit-animation-name:antZoomUpIn;animation-name:antZoomUpIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-up-leave.zoom-up-leave-active{-webkit-animation-name:antZoomUpOut;animation-name:antZoomUpOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-up-appear,.zoom-up-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-up-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-down-appear,.zoom-down-enter,.zoom-down-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-down-appear.zoom-down-appear-active,.zoom-down-enter.zoom-down-enter-active{-webkit-animation-name:antZoomDownIn;animation-name:antZoomDownIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-down-leave.zoom-down-leave-active{-webkit-animation-name:antZoomDownOut;animation-name:antZoomDownOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-down-appear,.zoom-down-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-down-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-left-appear,.zoom-left-enter,.zoom-left-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-left-appear.zoom-left-appear-active,.zoom-left-enter.zoom-left-enter-active{-webkit-animation-name:antZoomLeftIn;animation-name:antZoomLeftIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-left-leave.zoom-left-leave-active{-webkit-animation-name:antZoomLeftOut;animation-name:antZoomLeftOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-left-appear,.zoom-left-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-left-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}.zoom-right-appear,.zoom-right-enter,.zoom-right-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.zoom-right-appear.zoom-right-appear-active,.zoom-right-enter.zoom-right-enter-active{-webkit-animation-name:antZoomRightIn;animation-name:antZoomRightIn;-webkit-animation-play-state:running;animation-play-state:running}.zoom-right-leave.zoom-right-leave-active{-webkit-animation-name:antZoomRightOut;animation-name:antZoomRightOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.zoom-right-appear,.zoom-right-enter{-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-animation-timing-function:cubic-bezier(.08,.82,.17,1);animation-timing-function:cubic-bezier(.08,.82,.17,1)}.zoom-right-leave{-webkit-animation-timing-function:cubic-bezier(.78,.14,.15,.86);animation-timing-function:cubic-bezier(.78,.14,.15,.86)}@-webkit-keyframes antZoomIn{0%{-webkit-transform:scale(.2);transform:scale(.2);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes antZoomIn{0%{-webkit-transform:scale(.2);transform:scale(.2);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes antZoomOut{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.2);transform:scale(.2);opacity:0}}@keyframes antZoomOut{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.2);transform:scale(.2);opacity:0}}@-webkit-keyframes antZoomBigIn{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@keyframes antZoomBigIn{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}to{-webkit-transform:scale(1);transform:scale(1);opacity:1}}@-webkit-keyframes antZoomBigOut{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}}@keyframes antZoomBigOut{0%{-webkit-transform:scale(1);transform:scale(1)}to{-webkit-transform:scale(.8);transform:scale(.8);opacity:0}}@-webkit-keyframes antZoomUpIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 0;transform-origin:50% 0;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 0;transform-origin:50% 0}}@keyframes antZoomUpIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 0;transform-origin:50% 0;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 0;transform-origin:50% 0}}@-webkit-keyframes antZoomUpOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 0;transform-origin:50% 0}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 0;transform-origin:50% 0;opacity:0}}@keyframes antZoomUpOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 0;transform-origin:50% 0}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 0;transform-origin:50% 0;opacity:0}}@-webkit-keyframes antZoomLeftIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:0 50%;transform-origin:0 50%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:0 50%;transform-origin:0 50%}}@keyframes antZoomLeftIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:0 50%;transform-origin:0 50%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:0 50%;transform-origin:0 50%}}@-webkit-keyframes antZoomLeftOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:0 50%;transform-origin:0 50%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:0 50%;transform-origin:0 50%;opacity:0}}@keyframes antZoomLeftOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:0 50%;transform-origin:0 50%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:0 50%;transform-origin:0 50%;opacity:0}}@-webkit-keyframes antZoomRightIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:100% 50%;transform-origin:100% 50%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}}@keyframes antZoomRightIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:100% 50%;transform-origin:100% 50%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}}@-webkit-keyframes antZoomRightOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:100% 50%;transform-origin:100% 50%;opacity:0}}@keyframes antZoomRightOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:100% 50%;transform-origin:100% 50%;opacity:0}}@-webkit-keyframes antZoomDownIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 100%;transform-origin:50% 100%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 100%;transform-origin:50% 100%}}@keyframes antZoomDownIn{0%{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 100%;transform-origin:50% 100%;opacity:0}to{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 100%;transform-origin:50% 100%}}@-webkit-keyframes antZoomDownOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 100%;transform-origin:50% 100%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 100%;transform-origin:50% 100%;opacity:0}}@keyframes antZoomDownOut{0%{-webkit-transform:scale(1);transform:scale(1);-webkit-transform-origin:50% 100%;transform-origin:50% 100%}to{-webkit-transform:scale(.8);transform:scale(.8);-webkit-transform-origin:50% 100%;transform-origin:50% 100%;opacity:0}}.ant-motion-collapse-legacy{overflow:hidden}.ant-motion-collapse,.ant-motion-collapse-legacy-active{-webkit-transition:height .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1)!important;transition:height .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1)!important}.ant-motion-collapse{overflow:hidden}.ant-affix{position:fixed;z-index:10}.ant-alert{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;padding:8px 15px 8px 37px;word-wrap:break-word;border-radius:4px}.ant-alert.ant-alert-no-icon{padding:8px 15px}.ant-alert.ant-alert-closable{padding-right:30px}.ant-alert-icon{position:absolute;top:11.5px;left:16px}.ant-alert-description{display:none;font-size:14px;line-height:22px}.ant-alert-success{background-color:#f6ffed;border:1px solid #b7eb8f}.ant-alert-success .ant-alert-icon{color:#52c41a}.ant-alert-info{background-color:#e6f7ff;border:1px solid #91d5ff}.ant-alert-info .ant-alert-icon{color:#1890ff}.ant-alert-warning{background-color:#fffbe6;border:1px solid #ffe58f}.ant-alert-warning .ant-alert-icon{color:#faad14}.ant-alert-error{background-color:#fff1f0;border:1px solid #ffa39e}.ant-alert-error .ant-alert-icon{color:#f5222d}.ant-alert-close-icon{position:absolute;top:8px;right:16px;padding:0;overflow:hidden;font-size:12px;line-height:22px;background-color:transparent;border:none;outline:none;cursor:pointer}.ant-alert-close-icon .anticon-close{color:rgba(0,0,0,.45);-webkit-transition:color .3s;transition:color .3s}.ant-alert-close-icon .anticon-close:hover{color:rgba(0,0,0,.75)}.ant-alert-close-text{color:rgba(0,0,0,.45);-webkit-transition:color .3s;transition:color .3s}.ant-alert-close-text:hover{color:rgba(0,0,0,.75)}.ant-alert-with-description{position:relative;padding:15px 15px 15px 64px;color:rgba(0,0,0,.65);line-height:1.5;border-radius:4px}.ant-alert-with-description.ant-alert-no-icon{padding:15px}.ant-alert-with-description .ant-alert-icon{position:absolute;top:16px;left:24px;font-size:24px}.ant-alert-with-description .ant-alert-close-icon{position:absolute;top:16px;right:16px;font-size:14px;cursor:pointer}.ant-alert-with-description .ant-alert-message{display:block;margin-bottom:4px;color:rgba(0,0,0,.85);font-size:16px}.ant-alert-message{color:rgba(0,0,0,.85)}.ant-alert-with-description .ant-alert-description{display:block}.ant-alert.ant-alert-closing{height:0!important;margin:0;padding-top:0;padding-bottom:0;-webkit-transform-origin:50% 0;-ms-transform-origin:50% 0;transform-origin:50% 0;-webkit-transition:all .3s cubic-bezier(.78,.14,.15,.86);transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-alert-slide-up-leave{-webkit-animation:antAlertSlideUpOut .3s cubic-bezier(.78,.14,.15,.86);animation:antAlertSlideUpOut .3s cubic-bezier(.78,.14,.15,.86);-webkit-animation-fill-mode:both;animation-fill-mode:both}.ant-alert-banner{margin-bottom:0;border:0;border-radius:0}@-webkit-keyframes antAlertSlideUpIn{0%{-webkit-transform:scaleY(0);transform:scaleY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@keyframes antAlertSlideUpIn{0%{-webkit-transform:scaleY(0);transform:scaleY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}to{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}}@-webkit-keyframes antAlertSlideUpOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleY(0);transform:scaleY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}@keyframes antAlertSlideUpOut{0%{-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:1}to{-webkit-transform:scaleY(0);transform:scaleY(0);-webkit-transform-origin:0 0;transform-origin:0 0;opacity:0}}.ant-anchor{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;padding:0 0 0 2px}.ant-anchor-wrapper{margin-left:-4px;padding-left:4px;overflow:auto;background-color:#fff}.ant-anchor-ink{position:absolute;top:0;left:0;height:100%}.ant-anchor-ink:before{position:relative;display:block;width:2px;height:100%;margin:0 auto;background-color:#e8e8e8;content:" "}.ant-anchor-ink-ball{position:absolute;left:50%;display:none;width:8px;height:8px;background-color:#fff;border:2px solid #1890ff;border-radius:8px;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);-webkit-transition:top .3s ease-in-out;transition:top .3s ease-in-out}.ant-anchor-ink-ball.visible{display:inline-block}.ant-anchor.fixed .ant-anchor-ink .ant-anchor-ink-ball{display:none}.ant-anchor-link{padding:7px 0 7px 16px;line-height:1.143}.ant-anchor-link-title{position:relative;display:block;margin-bottom:6px;overflow:hidden;color:rgba(0,0,0,.65);white-space:nowrap;text-overflow:ellipsis;-webkit-transition:all .3s;transition:all .3s}.ant-anchor-link-title:only-child{margin-bottom:0}.ant-anchor-link-active>.ant-anchor-link-title{color:#1890ff}.ant-anchor-link .ant-anchor-link{padding-top:5px;padding-bottom:5px}.ant-select-auto-complete{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-select-auto-complete.ant-select .ant-select-selection{border:0;-webkit-box-shadow:none;box-shadow:none}.ant-select-auto-complete.ant-select .ant-select-selection__rendered{height:100%;margin-right:0;margin-left:0;line-height:32px}.ant-select-auto-complete.ant-select .ant-select-selection__placeholder{margin-right:12px;margin-left:12px}.ant-select-auto-complete.ant-select .ant-select-selection--single{height:auto}.ant-select-auto-complete.ant-select .ant-select-search--inline{position:static;float:left}.ant-select-auto-complete.ant-select-allow-clear .ant-select-selection:hover .ant-select-selection__rendered{margin-right:0!important}.ant-select-auto-complete.ant-select .ant-input{height:32px;line-height:1.5;background:transparent;border-width:1px}.ant-select-auto-complete.ant-select .ant-input:focus,.ant-select-auto-complete.ant-select .ant-input:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-select-auto-complete.ant-select .ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1;background-color:transparent}.ant-select-auto-complete.ant-select .ant-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-select-auto-complete.ant-select-lg .ant-select-selection__rendered{line-height:40px}.ant-select-auto-complete.ant-select-lg .ant-input{height:40px;padding-top:6px;padding-bottom:6px}.ant-select-auto-complete.ant-select-sm .ant-select-selection__rendered{line-height:24px}.ant-select-auto-complete.ant-select-sm .ant-input{height:24px;padding-top:1px;padding-bottom:1px}.ant-input-group>.ant-select-auto-complete .ant-select-search__field.ant-input-affix-wrapper{display:inline;float:none}.ant-select{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;outline:0}.ant-select,.ant-select ol,.ant-select ul{margin:0;padding:0;list-style:none}.ant-select>ul>li>a{padding:0;background-color:#fff}.ant-select-arrow{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;right:11px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:1;-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%}.ant-select-arrow>*{line-height:1}.ant-select-arrow svg{display:inline-block}.ant-select-arrow:before{display:none}.ant-select-arrow .ant-select-arrow-icon{display:block}.ant-select-arrow .ant-select-arrow-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-select-selection{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;background-color:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-radius:4px;outline:none;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-select-selection:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-select-focused .ant-select-selection,.ant-select-selection:active,.ant-select-selection:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-select-selection__clear{position:absolute;top:50%;right:11px;z-index:1;display:inline-block;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;font-style:normal;line-height:12px;text-align:center;text-transform:none;background:#fff;cursor:pointer;opacity:0;-webkit-transition:color .3s ease,opacity .15s ease;transition:color .3s ease,opacity .15s ease;text-rendering:auto}.ant-select-selection__clear:before{display:block}.ant-select-selection__clear:hover{color:rgba(0,0,0,.45)}.ant-select-selection:hover .ant-select-selection__clear{opacity:1}.ant-select-selection-selected-value{float:left;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-select-no-arrow .ant-select-selection-selected-value{padding-right:0}.ant-select-disabled{color:rgba(0,0,0,.25)}.ant-select-disabled .ant-select-selection{background:#f5f5f5;cursor:not-allowed}.ant-select-disabled .ant-select-selection:active,.ant-select-disabled .ant-select-selection:focus,.ant-select-disabled .ant-select-selection:hover{border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}.ant-select-disabled .ant-select-selection__clear{display:none;visibility:hidden;pointer-events:none}.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice{padding-right:10px;color:rgba(0,0,0,.33);background:#f5f5f5}.ant-select-disabled .ant-select-selection--multiple .ant-select-selection__choice__remove{display:none}.ant-select-selection--single{position:relative;height:32px;cursor:pointer}.ant-select-selection--single .ant-select-selection__rendered{margin-right:24px}.ant-select-no-arrow .ant-select-selection__rendered{margin-right:11px}.ant-select-selection__rendered{position:relative;display:block;margin-right:11px;margin-left:11px;line-height:30px}.ant-select-selection__rendered:after{display:inline-block;width:0;visibility:hidden;content:".";pointer-events:none}.ant-select-lg{font-size:16px}.ant-select-lg .ant-select-selection--single{height:40px}.ant-select-lg .ant-select-selection__rendered{line-height:38px}.ant-select-lg .ant-select-selection--multiple{min-height:40px}.ant-select-lg .ant-select-selection--multiple .ant-select-selection__rendered li{height:32px;line-height:32px}.ant-select-lg .ant-select-selection--multiple .ant-select-arrow,.ant-select-lg .ant-select-selection--multiple .ant-select-selection__clear{top:20px}.ant-select-sm .ant-select-selection--single{height:24px}.ant-select-sm .ant-select-selection__rendered{margin-left:7px;line-height:22px}.ant-select-sm .ant-select-selection--multiple{min-height:24px}.ant-select-sm .ant-select-selection--multiple .ant-select-selection__rendered li{height:16px;line-height:14px}.ant-select-sm .ant-select-selection--multiple .ant-select-arrow,.ant-select-sm .ant-select-selection--multiple .ant-select-selection__clear{top:12px}.ant-select-sm .ant-select-arrow,.ant-select-sm .ant-select-selection__clear{right:8px}.ant-select-disabled .ant-select-selection__choice__remove{color:rgba(0,0,0,.25);cursor:default}.ant-select-disabled .ant-select-selection__choice__remove:hover{color:rgba(0,0,0,.25)}.ant-select-search__field__wrap{position:relative;display:inline-block}.ant-select-search__field__placeholder,.ant-select-selection__placeholder{position:absolute;top:50%;right:9px;left:0;max-width:100%;height:20px;margin-top:-10px;overflow:hidden;color:#bfbfbf;line-height:20px;white-space:nowrap;text-align:left;text-overflow:ellipsis}.ant-select-search__field__placeholder{left:12px}.ant-select-search__field__mirror{position:absolute;top:0;left:0;white-space:pre;opacity:0;pointer-events:none}.ant-select-search--inline{position:absolute;width:100%;height:100%}.ant-select-search--inline .ant-select-search__field__wrap{width:100%;height:100%}.ant-select-search--inline .ant-select-search__field{width:100%;height:100%;font-size:100%;line-height:1;background:transparent;border-width:0;border-radius:4px;outline:0}.ant-select-search--inline>i{float:right}.ant-select-selection--multiple{min-height:32px;padding-bottom:3px;cursor:text;zoom:1}.ant-select-selection--multiple:after,.ant-select-selection--multiple:before{display:table;content:""}.ant-select-selection--multiple:after{clear:both}.ant-select-selection--multiple .ant-select-search--inline{position:static;float:left;width:auto;max-width:100%;padding:0}.ant-select-selection--multiple .ant-select-search--inline .ant-select-search__field{width:.75em;max-width:100%;padding:1px}.ant-select-selection--multiple .ant-select-selection__rendered{height:auto;margin-bottom:-3px;margin-left:5px}.ant-select-selection--multiple .ant-select-selection__placeholder{margin-left:6px}.ant-select-selection--multiple .ant-select-selection__rendered>ul>li,.ant-select-selection--multiple>ul>li{height:24px;margin-top:3px;line-height:22px}.ant-select-selection--multiple .ant-select-selection__choice{position:relative;float:left;max-width:99%;margin-right:4px;padding:0 20px 0 10px;overflow:hidden;color:rgba(0,0,0,.65);background-color:#fafafa;border:1px solid #e8e8e8;border-radius:2px;cursor:default;-webkit-transition:padding .3s cubic-bezier(.645,.045,.355,1);transition:padding .3s cubic-bezier(.645,.045,.355,1)}.ant-select-selection--multiple .ant-select-selection__choice__disabled{padding:0 10px}.ant-select-selection--multiple .ant-select-selection__choice__content{display:inline-block;max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-transition:margin .3s cubic-bezier(.645,.045,.355,1);transition:margin .3s cubic-bezier(.645,.045,.355,1)}.ant-select-selection--multiple .ant-select-selection__choice__remove{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;right:4px;color:rgba(0,0,0,.45);font-weight:700;line-height:inherit;cursor:pointer;-webkit-transition:all .3s;transition:all .3s;display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}.ant-select-selection--multiple .ant-select-selection__choice__remove>*{line-height:1}.ant-select-selection--multiple .ant-select-selection__choice__remove svg{display:inline-block}.ant-select-selection--multiple .ant-select-selection__choice__remove:before{display:none}.ant-select-selection--multiple .ant-select-selection__choice__remove .ant-select-selection--multiple .ant-select-selection__choice__remove-icon{display:block}:root .ant-select-selection--multiple .ant-select-selection__choice__remove{font-size:12px}.ant-select-selection--multiple .ant-select-selection__choice__remove:hover{color:rgba(0,0,0,.75)}.ant-select-selection--multiple .ant-select-arrow,.ant-select-selection--multiple .ant-select-selection__clear{top:16px}.ant-select-allow-clear .ant-select-selection--multiple .ant-select-selection__rendered,.ant-select-show-arrow .ant-select-selection--multiple .ant-select-selection__rendered{margin-right:20px}.ant-select-open .ant-select-arrow-icon svg{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.ant-select-open .ant-select-selection{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-select-combobox .ant-select-arrow{display:none}.ant-select-combobox .ant-select-search--inline{float:none;width:100%;height:100%}.ant-select-combobox .ant-select-search__field__wrap{width:100%;height:100%}.ant-select-combobox .ant-select-search__field{position:relative;z-index:1;width:100%;height:100%;-webkit-box-shadow:none;box-shadow:none;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1),height 0s;transition:all .3s cubic-bezier(.645,.045,.355,1),height 0s}.ant-select-combobox.ant-select-allow-clear .ant-select-selection:hover .ant-select-selection__rendered,.ant-select-combobox.ant-select-show-arrow .ant-select-selection:hover .ant-select-selection__rendered{margin-right:20px}.ant-select-dropdown{margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum",;position:absolute;top:-9999px;left:-9999px;z-index:1050;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;font-variant:normal;background-color:#fff;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-select-dropdown.slide-up-appear.slide-up-appear-active.ant-select-dropdown-placement-bottomLeft,.ant-select-dropdown.slide-up-enter.slide-up-enter-active.ant-select-dropdown-placement-bottomLeft{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn}.ant-select-dropdown.slide-up-appear.slide-up-appear-active.ant-select-dropdown-placement-topLeft,.ant-select-dropdown.slide-up-enter.slide-up-enter-active.ant-select-dropdown-placement-topLeft{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn}.ant-select-dropdown.slide-up-leave.slide-up-leave-active.ant-select-dropdown-placement-bottomLeft{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut}.ant-select-dropdown.slide-up-leave.slide-up-leave-active.ant-select-dropdown-placement-topLeft{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut}.ant-select-dropdown-hidden{display:none}.ant-select-dropdown-menu{max-height:250px;margin-bottom:0;padding:4px 0;overflow:auto;list-style:none;outline:none}.ant-select-dropdown-menu-item-group-list{margin:0;padding:0}.ant-select-dropdown-menu-item-group-list>.ant-select-dropdown-menu-item{padding-left:20px}.ant-select-dropdown-menu-item-group-title{height:32px;padding:0 12px;color:rgba(0,0,0,.45);font-size:12px;line-height:32px}.ant-select-dropdown-menu-item-group-list .ant-select-dropdown-menu-item:first-child:not(:last-child),.ant-select-dropdown-menu-item-group:not(:last-child) .ant-select-dropdown-menu-item-group-list .ant-select-dropdown-menu-item:last-child{border-radius:0}.ant-select-dropdown-menu-item{position:relative;display:block;padding:5px 12px;overflow:hidden;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;line-height:22px;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:#e6f7ff}.ant-select-dropdown-menu-item-selected{color:rgba(0,0,0,.65);font-weight:600;background-color:#fafafa}.ant-select-dropdown-menu-item-disabled,.ant-select-dropdown-menu-item-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled){background-color:#e6f7ff}.ant-select-dropdown-menu-item-divider{height:1px;margin:1px 0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item{padding-right:32px}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item .ant-select-selected-icon{position:absolute;top:50%;right:12px;color:transparent;font-weight:700;font-size:12px;text-shadow:0 .1px 0,.1px 0 0,0 -.1px 0,-.1px 0;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%);-webkit-transition:all .2s;transition:all .2s}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:rgba(0,0,0,.87)}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-disabled .ant-select-selected-icon{display:none}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected .ant-select-selected-icon,.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected:hover .ant-select-selected-icon{display:inline-block;color:#1890ff}.ant-select-dropdown--empty.ant-select-dropdown--multiple .ant-select-dropdown-menu-item{padding-right:12px}.ant-select-dropdown-container-open .ant-select-dropdown,.ant-select-dropdown-open .ant-select-dropdown{display:block}.ant-empty{margin:0 8px;font-size:14px;line-height:22px;text-align:center}.ant-empty-image{height:100px;margin-bottom:8px}.ant-empty-image img{height:100%}.ant-empty-image svg{height:100%;margin:auto}.ant-empty-description{margin:0}.ant-empty-footer{margin-top:16px}.ant-empty-normal{margin:32px 0;color:rgba(0,0,0,.25)}.ant-empty-normal .ant-empty-image{height:40px}.ant-empty-small{margin:8px 0;color:rgba(0,0,0,.25)}.ant-empty-small .ant-empty-image{height:35px}.ant-input{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;width:100%;height:32px;padding:4px 11px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s}.ant-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input:-ms-input-placeholder{color:#bfbfbf}.ant-input::-webkit-input-placeholder{color:#bfbfbf}.ant-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-input:placeholder-shown{text-overflow:ellipsis}.ant-input:focus,.ant-input:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-input:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-input-sm{height:24px;padding:1px 7px}.ant-input-group{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:table;width:100%;border-collapse:separate;border-spacing:0}.ant-input-group[class*=col-]{float:none;padding-right:0;padding-left:0}.ant-input-group>[class*=col-]{padding-right:8px}.ant-input-group>[class*=col-]:last-child{padding-right:0}.ant-input-group-addon,.ant-input-group-wrap,.ant-input-group>.ant-input{display:table-cell}.ant-input-group-addon:not(:first-child):not(:last-child),.ant-input-group-wrap:not(:first-child):not(:last-child),.ant-input-group>.ant-input:not(:first-child):not(:last-child){border-radius:0}.ant-input-group-addon,.ant-input-group-wrap{width:1px;white-space:nowrap;vertical-align:middle}.ant-input-group-wrap>*{display:block!important}.ant-input-group .ant-input{float:left;width:100%;margin-bottom:0;text-align:inherit}.ant-input-group .ant-input:focus,.ant-input-group .ant-input:hover{z-index:1;border-right-width:1px}.ant-input-group-addon{position:relative;padding:0 11px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;text-align:center;background-color:#fafafa;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s}.ant-input-group-addon .ant-select{margin:-5px -11px}.ant-input-group-addon .ant-select .ant-select-selection{margin:-1px;background-color:inherit;border:1px solid transparent;-webkit-box-shadow:none;box-shadow:none}.ant-input-group-addon .ant-select-focused .ant-select-selection,.ant-input-group-addon .ant-select-open .ant-select-selection{color:#1890ff}.ant-input-group-addon>i:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;content:""}.ant-input-group-addon:first-child,.ant-input-group-addon:first-child .ant-select .ant-select-selection,.ant-input-group>.ant-input:first-child,.ant-input-group>.ant-input:first-child .ant-select .ant-select-selection{border-top-right-radius:0;border-bottom-right-radius:0}.ant-input-group>.ant-input-affix-wrapper:not(:first-child) .ant-input{border-top-left-radius:0;border-bottom-left-radius:0}.ant-input-group>.ant-input-affix-wrapper:not(:last-child) .ant-input{border-top-right-radius:0;border-bottom-right-radius:0}.ant-input-group-addon:first-child{border-right:0}.ant-input-group-addon:last-child{border-left:0}.ant-input-group-addon:last-child,.ant-input-group-addon:last-child .ant-select .ant-select-selection,.ant-input-group>.ant-input:last-child,.ant-input-group>.ant-input:last-child .ant-select .ant-select-selection{border-top-left-radius:0;border-bottom-left-radius:0}.ant-input-group-lg .ant-input,.ant-input-group-lg>.ant-input-group-addon{height:40px;padding:6px 11px;font-size:16px}.ant-input-group-sm .ant-input,.ant-input-group-sm>.ant-input-group-addon{height:24px;padding:1px 7px}.ant-input-group-lg .ant-select-selection--single{height:40px}.ant-input-group-sm .ant-select-selection--single{height:24px}.ant-input-group .ant-input-affix-wrapper{display:table-cell;float:left;width:100%}.ant-input-group.ant-input-group-compact{display:block;zoom:1}.ant-input-group.ant-input-group-compact:after,.ant-input-group.ant-input-group-compact:before{display:table;content:""}.ant-input-group.ant-input-group-compact:after{clear:both}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child){border-right-width:1px}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child):hover,.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child):hover,.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child):focus,.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child):hover{z-index:1}.ant-input-group.ant-input-group-compact>*{display:inline-block;float:none;vertical-align:top;border-radius:0}.ant-input-group.ant-input-group-compact>:not(:last-child){margin-right:-1px;border-right-width:1px}.ant-input-group.ant-input-group-compact .ant-input{float:none}.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input{border-right-width:1px;border-radius:0}.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-calendar-picker .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-cascader-picker .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-input-group-wrapper .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor:focus,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper .ant-mention-editor:hover,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input:focus,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input:hover,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection:focus,.ant-input-group.ant-input-group-compact>.ant-select>.ant-select-selection:hover,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input:focus,.ant-input-group.ant-input-group-compact>.ant-time-picker .ant-time-picker-input:hover{z-index:1}.ant-input-group.ant-input-group-compact>.ant-calendar-picker:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper:first-child .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete:first-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-select:first-child>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker:first-child .ant-time-picker-input,.ant-input-group.ant-input-group-compact>:first-child{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-input-group.ant-input-group-compact>.ant-calendar-picker:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker-focused:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-cascader-picker:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-mention-wrapper:last-child .ant-mention-editor,.ant-input-group.ant-input-group-compact>.ant-select-auto-complete:last-child .ant-input,.ant-input-group.ant-input-group-compact>.ant-select:last-child>.ant-select-selection,.ant-input-group.ant-input-group-compact>.ant-time-picker:last-child .ant-time-picker-input,.ant-input-group.ant-input-group-compact>:last-child{border-right-width:1px;border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-input-group.ant-input-group-compact>.ant-select-auto-complete .ant-input{vertical-align:top}.ant-input-group-wrapper{display:inline-block;width:100%;text-align:start;vertical-align:top}.ant-input-affix-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;width:100%;text-align:start}.ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important}.ant-input-affix-wrapper .ant-input{position:relative;text-align:inherit}.ant-input-affix-wrapper .ant-input-prefix,.ant-input-affix-wrapper .ant-input-suffix{position:absolute;top:50%;z-index:2;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;color:rgba(0,0,0,.65);line-height:0;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-input-affix-wrapper .ant-input-prefix :not(.anticon),.ant-input-affix-wrapper .ant-input-suffix :not(.anticon){line-height:1.5}.ant-input-affix-wrapper .ant-input-disabled~.ant-input-suffix .anticon{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-input-affix-wrapper .ant-input-prefix{left:12px}.ant-input-affix-wrapper .ant-input-suffix{right:12px}.ant-input-affix-wrapper .ant-input:not(:first-child){padding-left:30px}.ant-input-affix-wrapper .ant-input:not(:last-child){padding-right:30px}.ant-input-affix-wrapper.ant-input-affix-wrapper-input-with-clear-btn .ant-input:not(:last-child){padding-right:49px}.ant-input-affix-wrapper.ant-input-affix-wrapper-textarea-with-clear-btn .ant-input{padding-right:22px}.ant-input-affix-wrapper .ant-input{min-height:100%}.ant-input-password-icon{color:rgba(0,0,0,.45);cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-input-password-icon:hover{color:#333}.ant-input-clear-icon{color:rgba(0,0,0,.25);font-size:12px;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;vertical-align:0}.ant-input-clear-icon:hover{color:rgba(0,0,0,.45)}.ant-input-clear-icon:active{color:rgba(0,0,0,.65)}.ant-input-clear-icon+i{margin-left:6px}.ant-input-textarea-clear-icon{color:rgba(0,0,0,.25);font-size:12px;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;position:absolute;top:0;right:0;margin:8px 8px 0 0}.ant-input-textarea-clear-icon:hover{color:rgba(0,0,0,.45)}.ant-input-textarea-clear-icon:active{color:rgba(0,0,0,.65)}.ant-input-textarea-clear-icon+i{margin-left:6px}.ant-input-search-icon{color:rgba(0,0,0,.45);cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-input-search-icon:hover{color:rgba(0,0,0,.8)}.ant-input-search-enter-button input{border-right:0}.ant-input-search-enter-button+.ant-input-group-addon,.ant-input-search-enter-button input+.ant-input-group-addon{padding:0;border:0}.ant-input-search-enter-button+.ant-input-group-addon .ant-input-search-button,.ant-input-search-enter-button input+.ant-input-group-addon .ant-input-search-button{border-top-left-radius:0;border-bottom-left-radius:0}.ant-btn{line-height:1.499;position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;-webkit-box-shadow:0 2px 0 rgba(0,0,0,.015);box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:manipulation;touch-action:manipulation;height:32px;padding:0 15px;font-size:14px;border-radius:4px;color:rgba(0,0,0,.65);background-color:#fff;border:1px solid #d9d9d9}.ant-btn>.anticon{line-height:1}.ant-btn,.ant-btn:active,.ant-btn:focus{outline:0}.ant-btn:not([disabled]):hover{text-decoration:none}.ant-btn:not([disabled]):active{outline:0;-webkit-box-shadow:none;box-shadow:none}.ant-btn.disabled,.ant-btn[disabled]{cursor:not-allowed}.ant-btn.disabled>*,.ant-btn[disabled]>*{pointer-events:none}.ant-btn-lg{height:40px;padding:0 15px;font-size:16px;border-radius:4px}.ant-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}.ant-btn>a:only-child{color:currentColor}.ant-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn:focus,.ant-btn:hover{color:#40a9ff;background-color:#fff;border-color:#40a9ff}.ant-btn:focus>a:only-child,.ant-btn:hover>a:only-child{color:currentColor}.ant-btn:focus>a:only-child:after,.ant-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn.active,.ant-btn:active{color:#096dd9;background-color:#fff;border-color:#096dd9}.ant-btn.active>a:only-child,.ant-btn:active>a:only-child{color:currentColor}.ant-btn.active>a:only-child:after,.ant-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-disabled,.ant-btn-disabled.active,.ant-btn-disabled:active,.ant-btn-disabled:focus,.ant-btn-disabled:hover,.ant-btn.disabled,.ant-btn.disabled.active,.ant-btn.disabled:active,.ant-btn.disabled:focus,.ant-btn.disabled:hover,.ant-btn[disabled],.ant-btn[disabled].active,.ant-btn[disabled]:active,.ant-btn[disabled]:focus,.ant-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-disabled.active>a:only-child,.ant-btn-disabled:active>a:only-child,.ant-btn-disabled:focus>a:only-child,.ant-btn-disabled:hover>a:only-child,.ant-btn-disabled>a:only-child,.ant-btn.disabled.active>a:only-child,.ant-btn.disabled:active>a:only-child,.ant-btn.disabled:focus>a:only-child,.ant-btn.disabled:hover>a:only-child,.ant-btn.disabled>a:only-child,.ant-btn[disabled].active>a:only-child,.ant-btn[disabled]:active>a:only-child,.ant-btn[disabled]:focus>a:only-child,.ant-btn[disabled]:hover>a:only-child,.ant-btn[disabled]>a:only-child{color:currentColor}.ant-btn-disabled.active>a:only-child:after,.ant-btn-disabled:active>a:only-child:after,.ant-btn-disabled:focus>a:only-child:after,.ant-btn-disabled:hover>a:only-child:after,.ant-btn-disabled>a:only-child:after,.ant-btn.disabled.active>a:only-child:after,.ant-btn.disabled:active>a:only-child:after,.ant-btn.disabled:focus>a:only-child:after,.ant-btn.disabled:hover>a:only-child:after,.ant-btn.disabled>a:only-child:after,.ant-btn[disabled].active>a:only-child:after,.ant-btn[disabled]:active>a:only-child:after,.ant-btn[disabled]:focus>a:only-child:after,.ant-btn[disabled]:hover>a:only-child:after,.ant-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn.active,.ant-btn:active,.ant-btn:focus,.ant-btn:hover{text-decoration:none;background:#fff}.ant-btn>i,.ant-btn>span{display:inline-block;-webkit-transition:margin-left .3s cubic-bezier(.645,.045,.355,1);transition:margin-left .3s cubic-bezier(.645,.045,.355,1);pointer-events:none}.ant-btn-primary{color:#fff;background-color:#1890ff;border-color:#1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);-webkit-box-shadow:0 2px 0 rgba(0,0,0,.045);box-shadow:0 2px 0 rgba(0,0,0,.045)}.ant-btn-primary>a:only-child{color:currentColor}.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary:focus,.ant-btn-primary:hover{color:#fff;background-color:#40a9ff;border-color:#40a9ff}.ant-btn-primary:focus>a:only-child,.ant-btn-primary:hover>a:only-child{color:currentColor}.ant-btn-primary:focus>a:only-child:after,.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary.active,.ant-btn-primary:active{color:#fff;background-color:#096dd9;border-color:#096dd9}.ant-btn-primary.active>a:only-child,.ant-btn-primary:active>a:only-child{color:currentColor}.ant-btn-primary.active>a:only-child:after,.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-primary-disabled,.ant-btn-primary-disabled.active,.ant-btn-primary-disabled:active,.ant-btn-primary-disabled:focus,.ant-btn-primary-disabled:hover,.ant-btn-primary.disabled,.ant-btn-primary.disabled.active,.ant-btn-primary.disabled:active,.ant-btn-primary.disabled:focus,.ant-btn-primary.disabled:hover,.ant-btn-primary[disabled],.ant-btn-primary[disabled].active,.ant-btn-primary[disabled]:active,.ant-btn-primary[disabled]:focus,.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-primary-disabled.active>a:only-child,.ant-btn-primary-disabled:active>a:only-child,.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-primary-disabled>a:only-child,.ant-btn-primary.disabled.active>a:only-child,.ant-btn-primary.disabled:active>a:only-child,.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-primary.disabled>a:only-child,.ant-btn-primary[disabled].active>a:only-child,.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-primary[disabled]>a:only-child{color:currentColor}.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-primary-disabled>a:only-child:after,.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-primary.disabled>a:only-child:after,.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child){border-right-color:#40a9ff;border-left-color:#40a9ff}.ant-btn-group .ant-btn-primary:not(:first-child):not(:last-child):disabled{border-color:#d9d9d9}.ant-btn-group .ant-btn-primary:first-child:not(:last-child){border-right-color:#40a9ff}.ant-btn-group .ant-btn-primary:first-child:not(:last-child)[disabled]{border-right-color:#d9d9d9}.ant-btn-group .ant-btn-primary+.ant-btn-primary,.ant-btn-group .ant-btn-primary:last-child:not(:first-child){border-left-color:#40a9ff}.ant-btn-group .ant-btn-primary+.ant-btn-primary[disabled],.ant-btn-group .ant-btn-primary:last-child:not(:first-child)[disabled]{border-left-color:#d9d9d9}.ant-btn-ghost{color:rgba(0,0,0,.65);background-color:transparent;border-color:#d9d9d9}.ant-btn-ghost>a:only-child{color:currentColor}.ant-btn-ghost>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost:focus,.ant-btn-ghost:hover{color:#40a9ff;background-color:transparent;border-color:#40a9ff}.ant-btn-ghost:focus>a:only-child,.ant-btn-ghost:hover>a:only-child{color:currentColor}.ant-btn-ghost:focus>a:only-child:after,.ant-btn-ghost:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost.active,.ant-btn-ghost:active{color:#096dd9;background-color:transparent;border-color:#096dd9}.ant-btn-ghost.active>a:only-child,.ant-btn-ghost:active>a:only-child{color:currentColor}.ant-btn-ghost.active>a:only-child:after,.ant-btn-ghost:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-ghost-disabled,.ant-btn-ghost-disabled.active,.ant-btn-ghost-disabled:active,.ant-btn-ghost-disabled:focus,.ant-btn-ghost-disabled:hover,.ant-btn-ghost.disabled,.ant-btn-ghost.disabled.active,.ant-btn-ghost.disabled:active,.ant-btn-ghost.disabled:focus,.ant-btn-ghost.disabled:hover,.ant-btn-ghost[disabled],.ant-btn-ghost[disabled].active,.ant-btn-ghost[disabled]:active,.ant-btn-ghost[disabled]:focus,.ant-btn-ghost[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-ghost-disabled.active>a:only-child,.ant-btn-ghost-disabled:active>a:only-child,.ant-btn-ghost-disabled:focus>a:only-child,.ant-btn-ghost-disabled:hover>a:only-child,.ant-btn-ghost-disabled>a:only-child,.ant-btn-ghost.disabled.active>a:only-child,.ant-btn-ghost.disabled:active>a:only-child,.ant-btn-ghost.disabled:focus>a:only-child,.ant-btn-ghost.disabled:hover>a:only-child,.ant-btn-ghost.disabled>a:only-child,.ant-btn-ghost[disabled].active>a:only-child,.ant-btn-ghost[disabled]:active>a:only-child,.ant-btn-ghost[disabled]:focus>a:only-child,.ant-btn-ghost[disabled]:hover>a:only-child,.ant-btn-ghost[disabled]>a:only-child{color:currentColor}.ant-btn-ghost-disabled.active>a:only-child:after,.ant-btn-ghost-disabled:active>a:only-child:after,.ant-btn-ghost-disabled:focus>a:only-child:after,.ant-btn-ghost-disabled:hover>a:only-child:after,.ant-btn-ghost-disabled>a:only-child:after,.ant-btn-ghost.disabled.active>a:only-child:after,.ant-btn-ghost.disabled:active>a:only-child:after,.ant-btn-ghost.disabled:focus>a:only-child:after,.ant-btn-ghost.disabled:hover>a:only-child:after,.ant-btn-ghost.disabled>a:only-child:after,.ant-btn-ghost[disabled].active>a:only-child:after,.ant-btn-ghost[disabled]:active>a:only-child:after,.ant-btn-ghost[disabled]:focus>a:only-child:after,.ant-btn-ghost[disabled]:hover>a:only-child:after,.ant-btn-ghost[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed{color:rgba(0,0,0,.65);background-color:#fff;border-color:#d9d9d9;border-style:dashed}.ant-btn-dashed>a:only-child{color:currentColor}.ant-btn-dashed>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed:focus,.ant-btn-dashed:hover{color:#40a9ff;background-color:#fff;border-color:#40a9ff}.ant-btn-dashed:focus>a:only-child,.ant-btn-dashed:hover>a:only-child{color:currentColor}.ant-btn-dashed:focus>a:only-child:after,.ant-btn-dashed:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed.active,.ant-btn-dashed:active{color:#096dd9;background-color:#fff;border-color:#096dd9}.ant-btn-dashed.active>a:only-child,.ant-btn-dashed:active>a:only-child{color:currentColor}.ant-btn-dashed.active>a:only-child:after,.ant-btn-dashed:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-dashed-disabled,.ant-btn-dashed-disabled.active,.ant-btn-dashed-disabled:active,.ant-btn-dashed-disabled:focus,.ant-btn-dashed-disabled:hover,.ant-btn-dashed.disabled,.ant-btn-dashed.disabled.active,.ant-btn-dashed.disabled:active,.ant-btn-dashed.disabled:focus,.ant-btn-dashed.disabled:hover,.ant-btn-dashed[disabled],.ant-btn-dashed[disabled].active,.ant-btn-dashed[disabled]:active,.ant-btn-dashed[disabled]:focus,.ant-btn-dashed[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-dashed-disabled.active>a:only-child,.ant-btn-dashed-disabled:active>a:only-child,.ant-btn-dashed-disabled:focus>a:only-child,.ant-btn-dashed-disabled:hover>a:only-child,.ant-btn-dashed-disabled>a:only-child,.ant-btn-dashed.disabled.active>a:only-child,.ant-btn-dashed.disabled:active>a:only-child,.ant-btn-dashed.disabled:focus>a:only-child,.ant-btn-dashed.disabled:hover>a:only-child,.ant-btn-dashed.disabled>a:only-child,.ant-btn-dashed[disabled].active>a:only-child,.ant-btn-dashed[disabled]:active>a:only-child,.ant-btn-dashed[disabled]:focus>a:only-child,.ant-btn-dashed[disabled]:hover>a:only-child,.ant-btn-dashed[disabled]>a:only-child{color:currentColor}.ant-btn-dashed-disabled.active>a:only-child:after,.ant-btn-dashed-disabled:active>a:only-child:after,.ant-btn-dashed-disabled:focus>a:only-child:after,.ant-btn-dashed-disabled:hover>a:only-child:after,.ant-btn-dashed-disabled>a:only-child:after,.ant-btn-dashed.disabled.active>a:only-child:after,.ant-btn-dashed.disabled:active>a:only-child:after,.ant-btn-dashed.disabled:focus>a:only-child:after,.ant-btn-dashed.disabled:hover>a:only-child:after,.ant-btn-dashed.disabled>a:only-child:after,.ant-btn-dashed[disabled].active>a:only-child:after,.ant-btn-dashed[disabled]:active>a:only-child:after,.ant-btn-dashed[disabled]:focus>a:only-child:after,.ant-btn-dashed[disabled]:hover>a:only-child:after,.ant-btn-dashed[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger{color:#fff;background-color:#ff4d4f;border-color:#ff4d4f;text-shadow:0 -1px 0 rgba(0,0,0,.12);-webkit-box-shadow:0 2px 0 rgba(0,0,0,.045);box-shadow:0 2px 0 rgba(0,0,0,.045)}.ant-btn-danger>a:only-child{color:currentColor}.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger:focus,.ant-btn-danger:hover{color:#fff;background-color:#ff7875;border-color:#ff7875}.ant-btn-danger:focus>a:only-child,.ant-btn-danger:hover>a:only-child{color:currentColor}.ant-btn-danger:focus>a:only-child:after,.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger.active,.ant-btn-danger:active{color:#fff;background-color:#d9363e;border-color:#d9363e}.ant-btn-danger.active>a:only-child,.ant-btn-danger:active>a:only-child{color:currentColor}.ant-btn-danger.active>a:only-child:after,.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-danger-disabled,.ant-btn-danger-disabled.active,.ant-btn-danger-disabled:active,.ant-btn-danger-disabled:focus,.ant-btn-danger-disabled:hover,.ant-btn-danger.disabled,.ant-btn-danger.disabled.active,.ant-btn-danger.disabled:active,.ant-btn-danger.disabled:focus,.ant-btn-danger.disabled:hover,.ant-btn-danger[disabled],.ant-btn-danger[disabled].active,.ant-btn-danger[disabled]:active,.ant-btn-danger[disabled]:focus,.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-danger-disabled.active>a:only-child,.ant-btn-danger-disabled:active>a:only-child,.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-danger-disabled>a:only-child,.ant-btn-danger.disabled.active>a:only-child,.ant-btn-danger.disabled:active>a:only-child,.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-danger.disabled>a:only-child,.ant-btn-danger[disabled].active>a:only-child,.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-danger[disabled]>a:only-child{color:currentColor}.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-danger-disabled>a:only-child:after,.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-danger.disabled>a:only-child:after,.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;-webkit-box-shadow:none;box-shadow:none}.ant-btn-link>a:only-child{color:currentColor}.ant-btn-link>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link:focus,.ant-btn-link:hover{color:#40a9ff;background-color:transparent;border-color:#40a9ff}.ant-btn-link:focus>a:only-child,.ant-btn-link:hover>a:only-child{color:currentColor}.ant-btn-link:focus>a:only-child:after,.ant-btn-link:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link.active,.ant-btn-link:active{color:#096dd9;background-color:transparent;border-color:#096dd9}.ant-btn-link.active>a:only-child,.ant-btn-link:active>a:only-child{color:currentColor}.ant-btn-link.active>a:only-child:after,.ant-btn-link:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-link-disabled,.ant-btn-link-disabled.active,.ant-btn-link-disabled:active,.ant-btn-link-disabled:focus,.ant-btn-link-disabled:hover,.ant-btn-link.disabled,.ant-btn-link.disabled.active,.ant-btn-link.disabled:active,.ant-btn-link.disabled:focus,.ant-btn-link.disabled:hover,.ant-btn-link[disabled],.ant-btn-link[disabled].active,.ant-btn-link[disabled]:active,.ant-btn-link[disabled]:focus,.ant-btn-link[disabled]:hover{background-color:#f5f5f5;border-color:#d9d9d9}.ant-btn-link:active,.ant-btn-link:focus,.ant-btn-link:hover{border-color:transparent}.ant-btn-link-disabled,.ant-btn-link-disabled.active,.ant-btn-link-disabled:active,.ant-btn-link-disabled:focus,.ant-btn-link-disabled:hover,.ant-btn-link.disabled,.ant-btn-link.disabled.active,.ant-btn-link.disabled:active,.ant-btn-link.disabled:focus,.ant-btn-link.disabled:hover,.ant-btn-link[disabled],.ant-btn-link[disabled].active,.ant-btn-link[disabled]:active,.ant-btn-link[disabled]:focus,.ant-btn-link[disabled]:hover{color:rgba(0,0,0,.25);background-color:transparent;border-color:transparent;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-link-disabled.active>a:only-child,.ant-btn-link-disabled:active>a:only-child,.ant-btn-link-disabled:focus>a:only-child,.ant-btn-link-disabled:hover>a:only-child,.ant-btn-link-disabled>a:only-child,.ant-btn-link.disabled.active>a:only-child,.ant-btn-link.disabled:active>a:only-child,.ant-btn-link.disabled:focus>a:only-child,.ant-btn-link.disabled:hover>a:only-child,.ant-btn-link.disabled>a:only-child,.ant-btn-link[disabled].active>a:only-child,.ant-btn-link[disabled]:active>a:only-child,.ant-btn-link[disabled]:focus>a:only-child,.ant-btn-link[disabled]:hover>a:only-child,.ant-btn-link[disabled]>a:only-child{color:currentColor}.ant-btn-link-disabled.active>a:only-child:after,.ant-btn-link-disabled:active>a:only-child:after,.ant-btn-link-disabled:focus>a:only-child:after,.ant-btn-link-disabled:hover>a:only-child:after,.ant-btn-link-disabled>a:only-child:after,.ant-btn-link.disabled.active>a:only-child:after,.ant-btn-link.disabled:active>a:only-child:after,.ant-btn-link.disabled:focus>a:only-child:after,.ant-btn-link.disabled:hover>a:only-child:after,.ant-btn-link.disabled>a:only-child:after,.ant-btn-link[disabled].active>a:only-child:after,.ant-btn-link[disabled]:active>a:only-child:after,.ant-btn-link[disabled]:focus>a:only-child:after,.ant-btn-link[disabled]:hover>a:only-child:after,.ant-btn-link[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-icon-only{width:32px;height:32px;padding:0;font-size:16px;border-radius:4px}.ant-btn-icon-only.ant-btn-lg{width:40px;height:40px;padding:0;font-size:18px;border-radius:4px}.ant-btn-icon-only.ant-btn-sm{width:24px;height:24px;padding:0;font-size:14px;border-radius:4px}.ant-btn-icon-only>i{vertical-align:middle}.ant-btn-round{height:32px;padding:0 16px;font-size:14px;border-radius:32px}.ant-btn-round.ant-btn-lg{height:40px;padding:0 20px;font-size:16px;border-radius:40px}.ant-btn-round.ant-btn-sm{height:24px;padding:0 12px;font-size:14px;border-radius:24px}.ant-btn-round.ant-btn-icon-only{width:auto}.ant-btn-circle,.ant-btn-circle-outline{min-width:32px;padding-right:0;padding-left:0;text-align:center;border-radius:50%}.ant-btn-circle-outline.ant-btn-lg,.ant-btn-circle.ant-btn-lg{min-width:40px;border-radius:50%}.ant-btn-circle-outline.ant-btn-sm,.ant-btn-circle.ant-btn-sm{min-width:24px;border-radius:50%}.ant-btn:before{position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;z-index:1;display:none;background:#fff;border-radius:inherit;opacity:.35;-webkit-transition:opacity .2s;transition:opacity .2s;content:"";pointer-events:none}.ant-btn .anticon{-webkit-transition:margin-left .3s cubic-bezier(.645,.045,.355,1);transition:margin-left .3s cubic-bezier(.645,.045,.355,1)}.ant-btn .anticon.anticon-minus>svg,.ant-btn .anticon.anticon-plus>svg{shape-rendering:optimizeSpeed}.ant-btn.ant-btn-loading{position:relative}.ant-btn.ant-btn-loading:not([disabled]){pointer-events:none}.ant-btn.ant-btn-loading:before{display:block}.ant-btn.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only){padding-left:29px}.ant-btn.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only) .anticon:not(:last-child){margin-left:-14px}.ant-btn-sm.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only){padding-left:24px}.ant-btn-sm.ant-btn-loading:not(.ant-btn-circle):not(.ant-btn-circle-outline):not(.ant-btn-icon-only) .anticon{margin-left:-17px}.ant-btn-group{display:inline-block}.ant-btn-group,.ant-btn-group>.ant-btn,.ant-btn-group>span>.ant-btn{position:relative}.ant-btn-group>.ant-btn.active,.ant-btn-group>.ant-btn:active,.ant-btn-group>.ant-btn:focus,.ant-btn-group>.ant-btn:hover,.ant-btn-group>span>.ant-btn.active,.ant-btn-group>span>.ant-btn:active,.ant-btn-group>span>.ant-btn:focus,.ant-btn-group>span>.ant-btn:hover{z-index:2}.ant-btn-group>.ant-btn:disabled,.ant-btn-group>span>.ant-btn:disabled{z-index:0}.ant-btn-group>.ant-btn-icon-only{font-size:14px}.ant-btn-group-lg>.ant-btn,.ant-btn-group-lg>span>.ant-btn{height:40px;padding:0 15px;font-size:16px;border-radius:0;line-height:38px}.ant-btn-group-lg>.ant-btn.ant-btn-icon-only{width:40px;height:40px;padding-right:0;padding-left:0}.ant-btn-group-sm>.ant-btn,.ant-btn-group-sm>span>.ant-btn{height:24px;padding:0 7px;font-size:14px;border-radius:0;line-height:22px}.ant-btn-group-sm>.ant-btn>.anticon,.ant-btn-group-sm>span>.ant-btn>.anticon{font-size:14px}.ant-btn-group-sm>.ant-btn.ant-btn-icon-only{width:24px;height:24px;padding-right:0;padding-left:0}.ant-btn+.ant-btn-group,.ant-btn-group+.ant-btn,.ant-btn-group+.ant-btn-group,.ant-btn-group .ant-btn+.ant-btn,.ant-btn-group .ant-btn+span,.ant-btn-group>span+span,.ant-btn-group span+.ant-btn{margin-left:-1px}.ant-btn-group .ant-btn-primary+.ant-btn:not(.ant-btn-primary):not([disabled]){border-left-color:transparent}.ant-btn-group .ant-btn{border-radius:0}.ant-btn-group>.ant-btn:first-child,.ant-btn-group>span:first-child>.ant-btn{margin-left:0}.ant-btn-group>.ant-btn:only-child,.ant-btn-group>span:only-child>.ant-btn{border-radius:4px}.ant-btn-group>.ant-btn:first-child:not(:last-child),.ant-btn-group>span:first-child:not(:last-child)>.ant-btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-btn-group>.ant-btn:last-child:not(:first-child),.ant-btn-group>span:last-child:not(:first-child)>.ant-btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-btn-group-sm>.ant-btn:only-child,.ant-btn-group-sm>span:only-child>.ant-btn{border-radius:4px}.ant-btn-group-sm>.ant-btn:first-child:not(:last-child),.ant-btn-group-sm>span:first-child:not(:last-child)>.ant-btn{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-btn-group-sm>.ant-btn:last-child:not(:first-child),.ant-btn-group-sm>span:last-child:not(:first-child)>.ant-btn{border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-btn-group>.ant-btn-group{float:left}.ant-btn-group>.ant-btn-group:not(:first-child):not(:last-child)>.ant-btn{border-radius:0}.ant-btn-group>.ant-btn-group:first-child:not(:last-child)>.ant-btn:last-child{padding-right:8px;border-top-right-radius:0;border-bottom-right-radius:0}.ant-btn-group>.ant-btn-group:last-child:not(:first-child)>.ant-btn:first-child{padding-left:8px;border-top-left-radius:0;border-bottom-left-radius:0}.ant-btn:active>span,.ant-btn:focus>span{position:relative}.ant-btn>.anticon+span,.ant-btn>span+.anticon{margin-left:8px}.ant-btn-background-ghost{color:#fff;background:transparent!important;border-color:#fff}.ant-btn-background-ghost.ant-btn-primary{color:#1890ff;background-color:transparent;border-color:#1890ff;text-shadow:none}.ant-btn-background-ghost.ant-btn-primary>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary:focus,.ant-btn-background-ghost.ant-btn-primary:hover{color:#40a9ff;background-color:transparent;border-color:#40a9ff}.ant-btn-background-ghost.ant-btn-primary:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary.active,.ant-btn-background-ghost.ant-btn-primary:active{color:#096dd9;background-color:transparent;border-color:#096dd9}.ant-btn-background-ghost.ant-btn-primary.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-primary-disabled,.ant-btn-background-ghost.ant-btn-primary-disabled.active,.ant-btn-background-ghost.ant-btn-primary-disabled:active,.ant-btn-background-ghost.ant-btn-primary-disabled:focus,.ant-btn-background-ghost.ant-btn-primary-disabled:hover,.ant-btn-background-ghost.ant-btn-primary.disabled,.ant-btn-background-ghost.ant-btn-primary.disabled.active,.ant-btn-background-ghost.ant-btn-primary.disabled:active,.ant-btn-background-ghost.ant-btn-primary.disabled:focus,.ant-btn-background-ghost.ant-btn-primary.disabled:hover,.ant-btn-background-ghost.ant-btn-primary[disabled],.ant-btn-background-ghost.ant-btn-primary[disabled].active,.ant-btn-background-ghost.ant-btn-primary[disabled]:active,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-primary-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-primary[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-primary-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-primary[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger{color:#ff4d4f;background-color:transparent;border-color:#ff4d4f;text-shadow:none}.ant-btn-background-ghost.ant-btn-danger>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger:focus,.ant-btn-background-ghost.ant-btn-danger:hover{color:#ff7875;background-color:transparent;border-color:#ff7875}.ant-btn-background-ghost.ant-btn-danger:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger.active,.ant-btn-background-ghost.ant-btn-danger:active{color:#d9363e;background-color:transparent;border-color:#d9363e}.ant-btn-background-ghost.ant-btn-danger.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-danger-disabled,.ant-btn-background-ghost.ant-btn-danger-disabled.active,.ant-btn-background-ghost.ant-btn-danger-disabled:active,.ant-btn-background-ghost.ant-btn-danger-disabled:focus,.ant-btn-background-ghost.ant-btn-danger-disabled:hover,.ant-btn-background-ghost.ant-btn-danger.disabled,.ant-btn-background-ghost.ant-btn-danger.disabled.active,.ant-btn-background-ghost.ant-btn-danger.disabled:active,.ant-btn-background-ghost.ant-btn-danger.disabled:focus,.ant-btn-background-ghost.ant-btn-danger.disabled:hover,.ant-btn-background-ghost.ant-btn-danger[disabled],.ant-btn-background-ghost.ant-btn-danger[disabled].active,.ant-btn-background-ghost.ant-btn-danger[disabled]:active,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-danger-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-danger[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-danger-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-danger[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link{color:#1890ff;background-color:transparent;border-color:transparent;text-shadow:none;color:#fff}.ant-btn-background-ghost.ant-btn-link>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link:focus,.ant-btn-background-ghost.ant-btn-link:hover{color:#40a9ff;background-color:transparent;border-color:transparent}.ant-btn-background-ghost.ant-btn-link:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link:hover>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link.active,.ant-btn-background-ghost.ant-btn-link:active{color:#096dd9;background-color:transparent;border-color:transparent}.ant-btn-background-ghost.ant-btn-link.active>a:only-child,.ant-btn-background-ghost.ant-btn-link:active>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-background-ghost.ant-btn-link-disabled,.ant-btn-background-ghost.ant-btn-link-disabled.active,.ant-btn-background-ghost.ant-btn-link-disabled:active,.ant-btn-background-ghost.ant-btn-link-disabled:focus,.ant-btn-background-ghost.ant-btn-link-disabled:hover,.ant-btn-background-ghost.ant-btn-link.disabled,.ant-btn-background-ghost.ant-btn-link.disabled.active,.ant-btn-background-ghost.ant-btn-link.disabled:active,.ant-btn-background-ghost.ant-btn-link.disabled:focus,.ant-btn-background-ghost.ant-btn-link.disabled:hover,.ant-btn-background-ghost.ant-btn-link[disabled],.ant-btn-background-ghost.ant-btn-link[disabled].active,.ant-btn-background-ghost.ant-btn-link[disabled]:active,.ant-btn-background-ghost.ant-btn-link[disabled]:focus,.ant-btn-background-ghost.ant-btn-link[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-btn-background-ghost.ant-btn-link-disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link-disabled>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled.active>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:active>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link.disabled>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled].active>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:active>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:focus>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]:hover>a:only-child,.ant-btn-background-ghost.ant-btn-link[disabled]>a:only-child{color:currentColor}.ant-btn-background-ghost.ant-btn-link-disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link-disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled.active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link.disabled>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled].active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:active>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:focus>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]:hover>a:only-child:after,.ant-btn-background-ghost.ant-btn-link[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-btn-two-chinese-chars:first-letter{letter-spacing:.34em}.ant-btn-two-chinese-chars>:not(.anticon){margin-right:-.34em;letter-spacing:.34em}.ant-btn-block{width:100%}.ant-btn:empty{vertical-align:top}a.ant-btn{padding-top:.1px;line-height:30px}a.ant-btn-lg{line-height:38px}a.ant-btn-sm{line-height:22px}.ant-avatar{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;overflow:hidden;color:#fff;white-space:nowrap;text-align:center;vertical-align:middle;background:#ccc;width:32px;height:32px;line-height:32px;border-radius:50%}.ant-avatar-image{background:transparent}.ant-avatar-string{position:absolute;left:50%;-webkit-transform-origin:0 center;-ms-transform-origin:0 center;transform-origin:0 center}.ant-avatar.ant-avatar-icon{font-size:18px}.ant-avatar-lg{width:40px;height:40px;line-height:40px;border-radius:50%}.ant-avatar-lg-string{position:absolute;left:50%;-webkit-transform-origin:0 center;-ms-transform-origin:0 center;transform-origin:0 center}.ant-avatar-lg.ant-avatar-icon{font-size:24px}.ant-avatar-sm{width:24px;height:24px;line-height:24px;border-radius:50%}.ant-avatar-sm-string{position:absolute;left:50%;-webkit-transform-origin:0 center;-ms-transform-origin:0 center;transform-origin:0 center}.ant-avatar-sm.ant-avatar-icon{font-size:14px}.ant-avatar-square{border-radius:4px}.ant-avatar>img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.ant-back-top{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:fixed;right:100px;bottom:50px;z-index:10;width:40px;height:40px;cursor:pointer}.ant-back-top-content{width:40px;height:40px;overflow:hidden;color:#fff;text-align:center;background-color:rgba(0,0,0,.45);border-radius:20px}.ant-back-top-content,.ant-back-top-content:hover{-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-back-top-content:hover{background-color:rgba(0,0,0,.65)}.ant-back-top-icon{width:14px;height:16px;margin:12px auto;background:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAoCAYAAACWwljjAAAABGdBTUEAALGPC/xhBQAAAbtJREFUWAntmMtKw0AUhhMvS5cuxILgQlRUpIggIoKIIoigG1eC+AA+jo+i6FIXBfeuXIgoeKVeitVWJX5HWhhDksnUpp3FDPyZk3Nm5nycmZKkXhAEOXSA3lG7muTeRzmfy6HneUvIhnYkQK+Q9NhAA0Opg0vBEhjBKHiyb8iGMyQMOYuK41BcBSypAL+MYXSKjtFAW7EAGEO3qN4uMQbbAkXiSfRQJ1H6a+yhlkKRcAoVFYiweYNjtCVQJJpBz2GCiPt7fBOZQpFgDpUikse5HgnkM4Fi4QX0Fpc5wf9EbLqpUCy4jMoJSXWhFwbMNgWKhVbRhy5jirhs9fy/oFhgHVVTJEs7RLZ8sSEoJm6iz7SZDMbJ+/OKERQTttCXQRLToRUmrKWCYuA2+jbN0MB4OQobYShfdTCgn/sL1K36M7TLrN3n+758aPy2rrpR6+/od5E8tf/A1uLS9aId5T7J3CNYihkQ4D9PiMdMC7mp4rjB9kjFjZp8BlnVHJBuO1yFXIV0FdDF3RlyFdJVQBdv5AxVdIsq8apiZ2PyYO1EVykesGfZEESsCkweyR8MUW+V8uJ1gkYipmpdP1pm2aJVPEGzAAAAAElFTkSuQmCC) 100%/100% no-repeat}@media screen and (max-width:768px){.ant-back-top{right:60px}}@media screen and (max-width:480px){.ant-back-top{right:20px}}.ant-badge{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;color:unset;line-height:1}.ant-badge-count{min-width:20px;height:20px;padding:0 6px;color:#fff;font-weight:400;font-size:12px;line-height:20px;white-space:nowrap;text-align:center;background:#f5222d;border-radius:10px;-webkit-box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px #fff}.ant-badge-count a,.ant-badge-count a:hover{color:#fff}.ant-badge-multiple-words{padding:0 8px}.ant-badge-dot{width:6px;height:6px;background:#f5222d;border-radius:100%;-webkit-box-shadow:0 0 0 1px #fff;box-shadow:0 0 0 1px #fff}.ant-badge-count,.ant-badge-dot,.ant-badge .ant-scroll-number-custom-component{position:absolute;top:0;right:0;z-index:1;-webkit-transform:translate(50%,-50%);-ms-transform:translate(50%,-50%);transform:translate(50%,-50%);-webkit-transform-origin:100% 0;-ms-transform-origin:100% 0;transform-origin:100% 0}.ant-badge-status{line-height:inherit;vertical-align:baseline}.ant-badge-status-dot{position:relative;top:-1px;display:inline-block;width:6px;height:6px;vertical-align:middle;border-radius:50%}.ant-badge-status-success{background-color:#52c41a}.ant-badge-status-processing{position:relative;background-color:#1890ff}.ant-badge-status-processing:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:50%;-webkit-animation:antStatusProcessing 1.2s ease-in-out infinite;animation:antStatusProcessing 1.2s ease-in-out infinite;content:""}.ant-badge-status-default{background-color:#d9d9d9}.ant-badge-status-error{background-color:#f5222d}.ant-badge-status-warning{background-color:#faad14}.ant-badge-status-magenta,.ant-badge-status-pink{background:#eb2f96}.ant-badge-status-red{background:#f5222d}.ant-badge-status-volcano{background:#fa541c}.ant-badge-status-orange{background:#fa8c16}.ant-badge-status-yellow{background:#fadb14}.ant-badge-status-gold{background:#faad14}.ant-badge-status-cyan{background:#13c2c2}.ant-badge-status-lime{background:#a0d911}.ant-badge-status-green{background:#52c41a}.ant-badge-status-blue{background:#1890ff}.ant-badge-status-geekblue{background:#2f54eb}.ant-badge-status-purple{background:#722ed1}.ant-badge-status-text{margin-left:8px;color:rgba(0,0,0,.65);font-size:14px}.ant-badge-zoom-appear,.ant-badge-zoom-enter{-webkit-animation:antZoomBadgeIn .3s cubic-bezier(.12,.4,.29,1.46);animation:antZoomBadgeIn .3s cubic-bezier(.12,.4,.29,1.46);-webkit-animation-fill-mode:both;animation-fill-mode:both}.ant-badge-zoom-leave{-webkit-animation:antZoomBadgeOut .3s cubic-bezier(.71,-.46,.88,.6);animation:antZoomBadgeOut .3s cubic-bezier(.71,-.46,.88,.6);-webkit-animation-fill-mode:both;animation-fill-mode:both}.ant-badge-not-a-wrapper:not(.ant-badge-status){vertical-align:middle}.ant-badge-not-a-wrapper .ant-scroll-number{position:relative;top:auto;display:block}.ant-badge-not-a-wrapper .ant-badge-count{-webkit-transform:none;-ms-transform:none;transform:none}@-webkit-keyframes antStatusProcessing{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:.5}to{-webkit-transform:scale(2.4);transform:scale(2.4);opacity:0}}@keyframes antStatusProcessing{0%{-webkit-transform:scale(.8);transform:scale(.8);opacity:.5}to{-webkit-transform:scale(2.4);transform:scale(2.4);opacity:0}}.ant-scroll-number{overflow:hidden}.ant-scroll-number-only{display:inline-block;height:20px;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-scroll-number-only>p.ant-scroll-number-only-unit{height:20px;margin:0}.ant-scroll-number-symbol{vertical-align:top}@-webkit-keyframes antZoomBadgeIn{0%{-webkit-transform:scale(0) translate(50%,-50%);transform:scale(0) translate(50%,-50%);opacity:0}to{-webkit-transform:scale(1) translate(50%,-50%);transform:scale(1) translate(50%,-50%)}}@keyframes antZoomBadgeIn{0%{-webkit-transform:scale(0) translate(50%,-50%);transform:scale(0) translate(50%,-50%);opacity:0}to{-webkit-transform:scale(1) translate(50%,-50%);transform:scale(1) translate(50%,-50%)}}@-webkit-keyframes antZoomBadgeOut{0%{-webkit-transform:scale(1) translate(50%,-50%);transform:scale(1) translate(50%,-50%)}to{-webkit-transform:scale(0) translate(50%,-50%);transform:scale(0) translate(50%,-50%);opacity:0}}@keyframes antZoomBadgeOut{0%{-webkit-transform:scale(1) translate(50%,-50%);transform:scale(1) translate(50%,-50%)}to{-webkit-transform:scale(0) translate(50%,-50%);transform:scale(0) translate(50%,-50%);opacity:0}}.ant-breadcrumb{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";color:rgba(0,0,0,.45);font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:rgba(0,0,0,.45);-webkit-transition:color .3s;transition:color .3s}.ant-breadcrumb a:hover{color:#40a9ff}.ant-breadcrumb>span:last-child,.ant-breadcrumb>span:last-child a{color:rgba(0,0,0,.65)}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:rgba(0,0,0,.45)}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-menu{-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;font-variant:tabular-nums;line-height:1.5;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";margin:0;padding:0;color:rgba(0,0,0,.65);line-height:0;list-style:none;background:#fff;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15);-webkit-transition:background .3s,width .2s;transition:background .3s,width .2s;zoom:1}.ant-menu:after,.ant-menu:before{display:table;content:""}.ant-menu:after{clear:both}.ant-menu ol,.ant-menu ul{margin:0;padding:0;list-style:none}.ant-menu-hidden{display:none}.ant-menu-item-group-title{padding:8px 16px;color:rgba(0,0,0,.45);font-size:14px;line-height:1.5;-webkit-transition:all .3s;transition:all .3s}.ant-menu-submenu,.ant-menu-submenu-inline{-webkit-transition:border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1);transition:border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-selected{color:#1890ff}.ant-menu-item:active,.ant-menu-submenu-title:active{background:#e6f7ff}.ant-menu-submenu .ant-menu-sub{cursor:auto;-webkit-transition:background .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1);transition:background .3s cubic-bezier(.645,.045,.355,1),padding .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-item>a{display:block;color:rgba(0,0,0,.65)}.ant-menu-item>a:hover{color:#1890ff}.ant-menu-item>a:before{position:absolute;top:0;right:0;bottom:0;left:0;background-color:transparent;content:""}.ant-menu-item>.ant-badge>a{color:rgba(0,0,0,.65)}.ant-menu-item>.ant-badge>a:hover{color:#1890ff}.ant-menu-item-divider{height:1px;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-active,.ant-menu-submenu-title:hover,.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open{color:#1890ff}.ant-menu-horizontal .ant-menu-item,.ant-menu-horizontal .ant-menu-submenu{margin-top:-1px}.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu .ant-menu-submenu-title:hover{background-color:transparent}.ant-menu-item-selected,.ant-menu-item-selected>a,.ant-menu-item-selected>a:hover{color:#1890ff}.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color:#e6f7ff}.ant-menu-inline,.ant-menu-vertical,.ant-menu-vertical-left{border-right:1px solid #e8e8e8}.ant-menu-vertical-right{border-left:1px solid #e8e8e8}.ant-menu-vertical-left.ant-menu-sub,.ant-menu-vertical-right.ant-menu-sub,.ant-menu-vertical.ant-menu-sub{min-width:160px;padding:0;border-right:0;-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ant-menu-vertical-left.ant-menu-sub .ant-menu-item,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item,.ant-menu-vertical.ant-menu-sub .ant-menu-item{left:0;margin-left:0;border-right:0}.ant-menu-vertical-left.ant-menu-sub .ant-menu-item:after,.ant-menu-vertical-right.ant-menu-sub .ant-menu-item:after,.ant-menu-vertical.ant-menu-sub .ant-menu-item:after{border-right:0}.ant-menu-vertical-left.ant-menu-sub>.ant-menu-item,.ant-menu-vertical-left.ant-menu-sub>.ant-menu-submenu,.ant-menu-vertical-right.ant-menu-sub>.ant-menu-item,.ant-menu-vertical-right.ant-menu-sub>.ant-menu-submenu,.ant-menu-vertical.ant-menu-sub>.ant-menu-item,.ant-menu-vertical.ant-menu-sub>.ant-menu-submenu{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ant-menu-horizontal.ant-menu-sub{min-width:114px}.ant-menu-item,.ant-menu-submenu-title{position:relative;display:block;margin:0;padding:0 20px;white-space:nowrap;cursor:pointer;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1),border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1),border-color .3s cubic-bezier(.645,.045,.355,1),background .3s cubic-bezier(.645,.045,.355,1),padding .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-item .anticon,.ant-menu-submenu-title .anticon{min-width:14px;margin-right:10px;font-size:14px;-webkit-transition:font-size .15s cubic-bezier(.215,.61,.355,1),margin .3s cubic-bezier(.645,.045,.355,1);transition:font-size .15s cubic-bezier(.215,.61,.355,1),margin .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-item .anticon+span,.ant-menu-submenu-title .anticon+span{opacity:1;-webkit-transition:opacity .3s cubic-bezier(.645,.045,.355,1),width .3s cubic-bezier(.645,.045,.355,1);transition:opacity .3s cubic-bezier(.645,.045,.355,1),width .3s cubic-bezier(.645,.045,.355,1)}.ant-menu>.ant-menu-item-divider{height:1px;margin:1px 0;padding:0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-menu-submenu-popup{position:absolute;z-index:1050;background:#fff;border-radius:4px}.ant-menu-submenu-popup .submenu-title-wrapper{padding-right:20px}.ant-menu-submenu-popup:before{position:absolute;top:-7px;right:0;bottom:0;left:0;opacity:.0001;content:" "}.ant-menu-submenu>.ant-menu{background-color:#fff;border-radius:4px}.ant-menu-submenu>.ant-menu-submenu-title:after{-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow{position:absolute;top:50%;right:16px;width:10px;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{position:absolute;width:6px;height:1.5px;background:#fff;background:rgba(0,0,0,.65)\9;background-image:-webkit-gradient(linear,left top,right top,from(rgba(0,0,0,.65)),to(rgba(0,0,0,.65)));background-image:linear-gradient(90deg,rgba(0,0,0,.65),rgba(0,0,0,.65));background-image:none\9;border-radius:2px;-webkit-transition:background .3s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:background .3s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:background .3s cubic-bezier(.645,.045,.355,1),transform .3s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1);transition:background .3s cubic-bezier(.645,.045,.355,1),transform .3s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);content:""}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{-webkit-transform:rotate(45deg) translateY(-2px);-ms-transform:rotate(45deg) translateY(-2px);transform:rotate(45deg) translateY(-2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{-webkit-transform:rotate(-45deg) translateY(2px);-ms-transform:rotate(-45deg) translateY(2px);transform:rotate(-45deg) translateY(2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-inline>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-left>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical-right>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before,.ant-menu-submenu-vertical>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:after,.ant-menu-submenu-vertical>.ant-menu-submenu-title:hover .ant-menu-submenu-arrow:before{background:-webkit-gradient(linear,left top,right top,from(#1890ff),to(#1890ff));background:linear-gradient(90deg,#1890ff,#1890ff)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{-webkit-transform:rotate(-45deg) translateX(2px);-ms-transform:rotate(-45deg) translateX(2px);transform:rotate(-45deg) translateX(2px)}.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{-webkit-transform:rotate(45deg) translateX(-2px);-ms-transform:rotate(45deg) translateX(-2px);transform:rotate(45deg) translateX(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow{-webkit-transform:translateY(-2px);-ms-transform:translateY(-2px);transform:translateY(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:after{-webkit-transform:rotate(-45deg) translateX(-2px);-ms-transform:rotate(-45deg) translateX(-2px);transform:rotate(-45deg) translateX(-2px)}.ant-menu-submenu-open.ant-menu-submenu-inline>.ant-menu-submenu-title .ant-menu-submenu-arrow:before{-webkit-transform:rotate(45deg) translateX(2px);-ms-transform:rotate(45deg) translateX(2px);transform:rotate(45deg) translateX(2px)}.ant-menu-vertical-left .ant-menu-submenu-selected,.ant-menu-vertical-left .ant-menu-submenu-selected>a,.ant-menu-vertical-right .ant-menu-submenu-selected,.ant-menu-vertical-right .ant-menu-submenu-selected>a,.ant-menu-vertical .ant-menu-submenu-selected,.ant-menu-vertical .ant-menu-submenu-selected>a{color:#1890ff}.ant-menu-horizontal{line-height:46px;white-space:nowrap;border:0;border-bottom:1px solid #e8e8e8;-webkit-box-shadow:none;box-shadow:none}.ant-menu-horizontal>.ant-menu-item,.ant-menu-horizontal>.ant-menu-submenu{position:relative;top:1px;display:inline-block;vertical-align:bottom;border-bottom:2px solid transparent}.ant-menu-horizontal>.ant-menu-item-active,.ant-menu-horizontal>.ant-menu-item-open,.ant-menu-horizontal>.ant-menu-item-selected,.ant-menu-horizontal>.ant-menu-item:hover,.ant-menu-horizontal>.ant-menu-submenu-active,.ant-menu-horizontal>.ant-menu-submenu-open,.ant-menu-horizontal>.ant-menu-submenu-selected,.ant-menu-horizontal>.ant-menu-submenu:hover{color:#1890ff;border-bottom:2px solid #1890ff}.ant-menu-horizontal>.ant-menu-item>a{display:block;color:rgba(0,0,0,.65)}.ant-menu-horizontal>.ant-menu-item>a:hover{color:#1890ff}.ant-menu-horizontal>.ant-menu-item>a:before{bottom:-2px}.ant-menu-horizontal>.ant-menu-item-selected>a{color:#1890ff}.ant-menu-horizontal:after{display:block;clear:both;height:0;content:"\20"}.ant-menu-inline .ant-menu-item,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical .ant-menu-item{position:relative}.ant-menu-inline .ant-menu-item:after,.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-vertical-right .ant-menu-item:after,.ant-menu-vertical .ant-menu-item:after{position:absolute;top:0;right:0;bottom:0;border-right:3px solid #1890ff;-webkit-transform:scaleY(.0001);-ms-transform:scaleY(.0001);transform:scaleY(.0001);opacity:0;-webkit-transition:opacity .15s cubic-bezier(.215,.61,.355,1),-webkit-transform .15s cubic-bezier(.215,.61,.355,1);transition:opacity .15s cubic-bezier(.215,.61,.355,1),-webkit-transform .15s cubic-bezier(.215,.61,.355,1);transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1);transition:transform .15s cubic-bezier(.215,.61,.355,1),opacity .15s cubic-bezier(.215,.61,.355,1),-webkit-transform .15s cubic-bezier(.215,.61,.355,1);content:""}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title,.ant-menu-vertical-left .ant-menu-item,.ant-menu-vertical-left .ant-menu-submenu-title,.ant-menu-vertical-right .ant-menu-item,.ant-menu-vertical-right .ant-menu-submenu-title,.ant-menu-vertical .ant-menu-item,.ant-menu-vertical .ant-menu-submenu-title{height:40px;margin-top:4px;margin-bottom:4px;padding:0 16px;overflow:hidden;font-size:14px;line-height:40px;text-overflow:ellipsis}.ant-menu-inline .ant-menu-submenu,.ant-menu-vertical-left .ant-menu-submenu,.ant-menu-vertical-right .ant-menu-submenu,.ant-menu-vertical .ant-menu-submenu{padding-bottom:.02px}.ant-menu-inline .ant-menu-item:not(:last-child),.ant-menu-vertical-left .ant-menu-item:not(:last-child),.ant-menu-vertical-right .ant-menu-item:not(:last-child),.ant-menu-vertical .ant-menu-item:not(:last-child){margin-bottom:8px}.ant-menu-inline>.ant-menu-item,.ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical-left>.ant-menu-item,.ant-menu-vertical-left>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical-right>.ant-menu-item,.ant-menu-vertical-right>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-vertical>.ant-menu-item,.ant-menu-vertical>.ant-menu-submenu>.ant-menu-submenu-title{height:40px;line-height:40px}.ant-menu-inline{width:100%}.ant-menu-inline .ant-menu-item-selected:after,.ant-menu-inline .ant-menu-selected:after{-webkit-transform:scaleY(1);-ms-transform:scaleY(1);transform:scaleY(1);opacity:1;-webkit-transition:opacity .15s cubic-bezier(.645,.045,.355,1),-webkit-transform .15s cubic-bezier(.645,.045,.355,1);transition:opacity .15s cubic-bezier(.645,.045,.355,1),-webkit-transform .15s cubic-bezier(.645,.045,.355,1);transition:transform .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1);transition:transform .15s cubic-bezier(.645,.045,.355,1),opacity .15s cubic-bezier(.645,.045,.355,1),-webkit-transform .15s cubic-bezier(.645,.045,.355,1)}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title{width:calc(100% + 1px)}.ant-menu-inline .ant-menu-submenu-title{padding-right:34px}.ant-menu-inline-collapsed{width:80px}.ant-menu-inline-collapsed>.ant-menu-item,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title{left:0;padding:0 32px!important;text-overflow:clip}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-item .ant-menu-submenu-arrow,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .ant-menu-submenu-arrow{display:none}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .anticon,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .anticon,.ant-menu-inline-collapsed>.ant-menu-item .anticon,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .anticon{margin:0;font-size:16px;line-height:40px}.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-item .anticon+span,.ant-menu-inline-collapsed>.ant-menu-item-group>.ant-menu-item-group-list>.ant-menu-submenu>.ant-menu-submenu-title .anticon+span,.ant-menu-inline-collapsed>.ant-menu-item .anticon+span,.ant-menu-inline-collapsed>.ant-menu-submenu>.ant-menu-submenu-title .anticon+span{display:inline-block;max-width:0;opacity:0}.ant-menu-inline-collapsed-tooltip{pointer-events:none}.ant-menu-inline-collapsed-tooltip .anticon{display:none}.ant-menu-inline-collapsed-tooltip a{color:hsla(0,0%,100%,.85)}.ant-menu-inline-collapsed .ant-menu-item-group-title{padding-right:4px;padding-left:4px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-menu-item-group-list{margin:0;padding:0}.ant-menu-item-group-list .ant-menu-item,.ant-menu-item-group-list .ant-menu-submenu-title{padding:0 16px 0 28px}.ant-menu-root.ant-menu-inline,.ant-menu-root.ant-menu-vertical,.ant-menu-root.ant-menu-vertical-left,.ant-menu-root.ant-menu-vertical-right,.ant-menu-sub.ant-menu-inline{-webkit-box-shadow:none;box-shadow:none}.ant-menu-sub.ant-menu-inline{padding:0;border:0;border-radius:0}.ant-menu-sub.ant-menu-inline>.ant-menu-item,.ant-menu-sub.ant-menu-inline>.ant-menu-submenu>.ant-menu-submenu-title{height:40px;line-height:40px;list-style-position:inside;list-style-type:disc}.ant-menu-sub.ant-menu-inline .ant-menu-item-group-title{padding-left:32px}.ant-menu-item-disabled,.ant-menu-submenu-disabled{color:rgba(0,0,0,.25)!important;background:none;border-color:transparent!important;cursor:not-allowed}.ant-menu-item-disabled>a,.ant-menu-submenu-disabled>a{color:rgba(0,0,0,.25)!important;pointer-events:none}.ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-submenu-disabled>.ant-menu-submenu-title{color:rgba(0,0,0,.25)!important;cursor:not-allowed}.ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:rgba(0,0,0,.25)!important}.ant-menu-dark,.ant-menu-dark .ant-menu-sub{color:hsla(0,0%,100%,.65);background:#001529}.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow{opacity:.45;-webkit-transition:all .3s;transition:all .3s}.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-sub .ant-menu-submenu-title .ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title .ant-menu-submenu-arrow:before{background:#fff}.ant-menu-dark.ant-menu-submenu-popup{background:transparent}.ant-menu-dark .ant-menu-inline.ant-menu-sub{background:#000c17;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.45) inset;box-shadow:inset 0 2px 8px rgba(0,0,0,.45)}.ant-menu-dark.ant-menu-horizontal{border-bottom:0}.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu{top:0;margin-top:0;border-color:#001529;border-bottom:0}.ant-menu-dark.ant-menu-horizontal>.ant-menu-item>a:before{bottom:0}.ant-menu-dark .ant-menu-item,.ant-menu-dark .ant-menu-item-group-title,.ant-menu-dark .ant-menu-item>a{color:hsla(0,0%,100%,.65)}.ant-menu-dark.ant-menu-inline,.ant-menu-dark.ant-menu-vertical,.ant-menu-dark.ant-menu-vertical-left,.ant-menu-dark.ant-menu-vertical-right{border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item,.ant-menu-dark.ant-menu-vertical-left .ant-menu-item,.ant-menu-dark.ant-menu-vertical-right .ant-menu-item,.ant-menu-dark.ant-menu-vertical .ant-menu-item{left:0;margin-left:0;border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical-left .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical-right .ant-menu-item:after,.ant-menu-dark.ant-menu-vertical .ant-menu-item:after{border-right:0}.ant-menu-dark.ant-menu-inline .ant-menu-item,.ant-menu-dark.ant-menu-inline .ant-menu-submenu-title{width:100%}.ant-menu-dark .ant-menu-item-active,.ant-menu-dark .ant-menu-item:hover,.ant-menu-dark .ant-menu-submenu-active,.ant-menu-dark .ant-menu-submenu-open,.ant-menu-dark .ant-menu-submenu-selected,.ant-menu-dark .ant-menu-submenu-title:hover{color:#fff;background-color:transparent}.ant-menu-dark .ant-menu-item-active>a,.ant-menu-dark .ant-menu-item:hover>a,.ant-menu-dark .ant-menu-submenu-active>a,.ant-menu-dark .ant-menu-submenu-open>a,.ant-menu-dark .ant-menu-submenu-selected>a,.ant-menu-dark .ant-menu-submenu-title:hover>a{color:#fff}.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow{opacity:1}.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-active>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-open>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-selected>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title:hover>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-title:hover>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:#fff}.ant-menu-dark .ant-menu-item:hover{background-color:transparent}.ant-menu-dark .ant-menu-item-selected{color:#fff;border-right:0}.ant-menu-dark .ant-menu-item-selected:after{border-right:0}.ant-menu-dark .ant-menu-item-selected .anticon,.ant-menu-dark .ant-menu-item-selected .anticon+span,.ant-menu-dark .ant-menu-item-selected>a,.ant-menu-dark .ant-menu-item-selected>a:hover{color:#fff}.ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected,.ant-menu.ant-menu-dark .ant-menu-item-selected{background-color:#1890ff}.ant-menu-dark .ant-menu-item-disabled,.ant-menu-dark .ant-menu-item-disabled>a,.ant-menu-dark .ant-menu-submenu-disabled,.ant-menu-dark .ant-menu-submenu-disabled>a{color:hsla(0,0%,100%,.35)!important;opacity:.8}.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title{color:hsla(0,0%,100%,.35)!important}.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-item-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:after,.ant-menu-dark .ant-menu-submenu-disabled>.ant-menu-submenu-title>.ant-menu-submenu-arrow:before{background:hsla(0,0%,100%,.35)!important}.ant-tooltip{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;z-index:1060;display:block;max-width:250px;visibility:visible}.ant-tooltip-hidden{display:none}.ant-tooltip-placement-top,.ant-tooltip-placement-topLeft,.ant-tooltip-placement-topRight{padding-bottom:8px}.ant-tooltip-placement-right,.ant-tooltip-placement-rightBottom,.ant-tooltip-placement-rightTop{padding-left:8px}.ant-tooltip-placement-bottom,.ant-tooltip-placement-bottomLeft,.ant-tooltip-placement-bottomRight{padding-top:8px}.ant-tooltip-placement-left,.ant-tooltip-placement-leftBottom,.ant-tooltip-placement-leftTop{padding-right:8px}.ant-tooltip-inner{min-width:30px;min-height:32px;padding:6px 8px;color:#fff;text-align:left;text-decoration:none;word-wrap:break-word;background-color:rgba(0,0,0,.75);border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-tooltip-arrow{position:absolute;display:block;width:13.07106781px;height:13.07106781px;overflow:hidden;background:transparent;pointer-events:none}.ant-tooltip-arrow:before{position:absolute;top:0;right:0;bottom:0;left:0;display:block;width:5px;height:5px;margin:auto;background-color:rgba(0,0,0,.75);content:"";pointer-events:auto}.ant-tooltip-placement-top .ant-tooltip-arrow,.ant-tooltip-placement-topLeft .ant-tooltip-arrow,.ant-tooltip-placement-topRight .ant-tooltip-arrow{bottom:-5.07106781px}.ant-tooltip-placement-top .ant-tooltip-arrow:before,.ant-tooltip-placement-topLeft .ant-tooltip-arrow:before,.ant-tooltip-placement-topRight .ant-tooltip-arrow:before{-webkit-box-shadow:3px 3px 7px rgba(0,0,0,.07);box-shadow:3px 3px 7px rgba(0,0,0,.07);-webkit-transform:translateY(-6.53553391px) rotate(45deg);-ms-transform:translateY(-6.53553391px) rotate(45deg);transform:translateY(-6.53553391px) rotate(45deg)}.ant-tooltip-placement-top .ant-tooltip-arrow{left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ant-tooltip-placement-topLeft .ant-tooltip-arrow{left:13px}.ant-tooltip-placement-topRight .ant-tooltip-arrow{right:13px}.ant-tooltip-placement-right .ant-tooltip-arrow,.ant-tooltip-placement-rightBottom .ant-tooltip-arrow,.ant-tooltip-placement-rightTop .ant-tooltip-arrow{left:-5.07106781px}.ant-tooltip-placement-right .ant-tooltip-arrow:before,.ant-tooltip-placement-rightBottom .ant-tooltip-arrow:before,.ant-tooltip-placement-rightTop .ant-tooltip-arrow:before{-webkit-box-shadow:-3px 3px 7px rgba(0,0,0,.07);box-shadow:-3px 3px 7px rgba(0,0,0,.07);-webkit-transform:translateX(6.53553391px) rotate(45deg);-ms-transform:translateX(6.53553391px) rotate(45deg);transform:translateX(6.53553391px) rotate(45deg)}.ant-tooltip-placement-right .ant-tooltip-arrow{top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-tooltip-placement-rightTop .ant-tooltip-arrow{top:5px}.ant-tooltip-placement-rightBottom .ant-tooltip-arrow{bottom:5px}.ant-tooltip-placement-left .ant-tooltip-arrow,.ant-tooltip-placement-leftBottom .ant-tooltip-arrow,.ant-tooltip-placement-leftTop .ant-tooltip-arrow{right:-5.07106781px}.ant-tooltip-placement-left .ant-tooltip-arrow:before,.ant-tooltip-placement-leftBottom .ant-tooltip-arrow:before,.ant-tooltip-placement-leftTop .ant-tooltip-arrow:before{-webkit-box-shadow:3px -3px 7px rgba(0,0,0,.07);box-shadow:3px -3px 7px rgba(0,0,0,.07);-webkit-transform:translateX(-6.53553391px) rotate(45deg);-ms-transform:translateX(-6.53553391px) rotate(45deg);transform:translateX(-6.53553391px) rotate(45deg)}.ant-tooltip-placement-left .ant-tooltip-arrow{top:50%;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-tooltip-placement-leftTop .ant-tooltip-arrow{top:5px}.ant-tooltip-placement-leftBottom .ant-tooltip-arrow{bottom:5px}.ant-tooltip-placement-bottom .ant-tooltip-arrow,.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow,.ant-tooltip-placement-bottomRight .ant-tooltip-arrow{top:-5.07106781px}.ant-tooltip-placement-bottom .ant-tooltip-arrow:before,.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow:before,.ant-tooltip-placement-bottomRight .ant-tooltip-arrow:before{-webkit-box-shadow:-3px -3px 7px rgba(0,0,0,.07);box-shadow:-3px -3px 7px rgba(0,0,0,.07);-webkit-transform:translateY(6.53553391px) rotate(45deg);-ms-transform:translateY(6.53553391px) rotate(45deg);transform:translateY(6.53553391px) rotate(45deg)}.ant-tooltip-placement-bottom .ant-tooltip-arrow{left:50%;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ant-tooltip-placement-bottomLeft .ant-tooltip-arrow{left:13px}.ant-tooltip-placement-bottomRight .ant-tooltip-arrow{right:13px}.ant-dropdown{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;top:-9999px;left:-9999px;z-index:1050;display:block}.ant-dropdown:before{position:absolute;top:-7px;right:0;bottom:-7px;left:-7px;z-index:-9999;opacity:.0001;content:" "}.ant-dropdown-wrap{position:relative}.ant-dropdown-wrap .ant-btn>.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-wrap .ant-btn>.anticon-down{font-size:12px}.ant-dropdown-wrap .anticon-down:before{-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s}.ant-dropdown-wrap-open .anticon-down:before{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.ant-dropdown-hidden,.ant-dropdown-menu-hidden{display:none}.ant-dropdown-menu{position:relative;margin:0;padding:4px 0;text-align:left;list-style-type:none;background-color:#fff;background-clip:padding-box;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15);-webkit-transform:translateZ(0)}.ant-dropdown-menu-item-group-title{padding:5px 12px;color:rgba(0,0,0,.45);-webkit-transition:all .3s;transition:all .3s}.ant-dropdown-menu-submenu-popup{position:absolute;z-index:1050}.ant-dropdown-menu-submenu-popup>.ant-dropdown-menu{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ant-dropdown-menu-submenu-popup li,.ant-dropdown-menu-submenu-popup ul{list-style:none}.ant-dropdown-menu-submenu-popup ul{margin-right:.3em;margin-left:.3em;padding:0}.ant-dropdown-menu-item,.ant-dropdown-menu-submenu-title{clear:both;margin:0;padding:5px 12px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px;line-height:22px;white-space:nowrap;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-dropdown-menu-item>.anticon:first-child,.ant-dropdown-menu-item>span>.anticon:first-child,.ant-dropdown-menu-submenu-title>.anticon:first-child,.ant-dropdown-menu-submenu-title>span>.anticon:first-child{min-width:12px;margin-right:8px;font-size:12px}.ant-dropdown-menu-item>a,.ant-dropdown-menu-submenu-title>a{display:block;margin:-5px -12px;padding:5px 12px;color:rgba(0,0,0,.65);-webkit-transition:all .3s;transition:all .3s}.ant-dropdown-menu-item-selected,.ant-dropdown-menu-item-selected>a,.ant-dropdown-menu-submenu-title-selected,.ant-dropdown-menu-submenu-title-selected>a{color:#1890ff;background-color:#e6f7ff}.ant-dropdown-menu-item:hover,.ant-dropdown-menu-submenu-title:hover{background-color:#e6f7ff}.ant-dropdown-menu-item-disabled,.ant-dropdown-menu-submenu-title-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-dropdown-menu-item-disabled:hover,.ant-dropdown-menu-submenu-title-disabled:hover{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-dropdown-menu-item-divider,.ant-dropdown-menu-submenu-title-divider{height:1px;margin:4px 0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow,.ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow{position:absolute;right:8px}.ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow-icon,.ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{color:rgba(0,0,0,.45);font-style:normal;display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow-icon,:root .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{font-size:12px}.ant-dropdown-menu-item-group-list{margin:0 8px;padding:0;list-style:none}.ant-dropdown-menu-submenu-title{padding-right:26px}.ant-dropdown-menu-submenu-vertical{position:relative}.ant-dropdown-menu-submenu-vertical>.ant-dropdown-menu{position:absolute;top:0;left:100%;min-width:100%;margin-left:4px;-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ant-dropdown-menu-submenu.ant-dropdown-menu-submenu-disabled .ant-dropdown-menu-submenu-title,.ant-dropdown-menu-submenu.ant-dropdown-menu-submenu-disabled .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow-icon{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-dropdown-menu-submenu-selected .ant-dropdown-menu-submenu-title{color:#1890ff}.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-appear.slide-down-appear-active.ant-dropdown-placement-bottomRight,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-enter.slide-down-enter-active.ant-dropdown-placement-bottomRight{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn}.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-appear.slide-up-appear-active.ant-dropdown-placement-topRight,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-enter.slide-up-enter-active.ant-dropdown-placement-topRight{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn}.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomCenter,.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomLeft,.ant-dropdown.slide-down-leave.slide-down-leave-active.ant-dropdown-placement-bottomRight{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut}.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topCenter,.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topLeft,.ant-dropdown.slide-up-leave.slide-up-leave-active.ant-dropdown-placement-topRight{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut}.ant-dropdown-link>.anticon.anticon-down,.ant-dropdown-trigger>.anticon.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-link>.anticon.anticon-down,:root .ant-dropdown-trigger>.anticon.anticon-down{font-size:12px}.ant-dropdown-button{white-space:nowrap}.ant-dropdown-button.ant-btn-group>.ant-btn:last-child:not(:first-child){padding-right:8px;padding-left:8px}.ant-dropdown-button .anticon.anticon-down{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}:root .ant-dropdown-button .anticon.anticon-down{font-size:12px}.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu{background:#001529}.ant-dropdown-menu-dark .ant-dropdown-menu-item,.ant-dropdown-menu-dark .ant-dropdown-menu-item .ant-dropdown-menu-submenu-arrow:after,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a .ant-dropdown-menu-submenu-arrow:after,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title .ant-dropdown-menu-submenu-arrow:after{color:hsla(0,0%,100%,.65)}.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-item>a:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-submenu-title:hover{color:#fff;background:transparent}.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected,.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected:hover,.ant-dropdown-menu-dark .ant-dropdown-menu-item-selected>a{color:#fff;background:#1890ff}.ant-fullcalendar{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";border-top:1px solid #d9d9d9;outline:none}.ant-select.ant-fullcalendar-year-select{min-width:90px}.ant-select.ant-fullcalendar-year-select.ant-select-sm{min-width:70px}.ant-select.ant-fullcalendar-month-select{min-width:80px;margin-left:8px}.ant-select.ant-fullcalendar-month-select.ant-select-sm{min-width:70px}.ant-fullcalendar-header{padding:11px 16px 11px 0;text-align:right}.ant-fullcalendar-header .ant-select-dropdown{text-align:left}.ant-fullcalendar-header .ant-radio-group{margin-left:8px;text-align:left}.ant-fullcalendar-header label.ant-radio-button{height:22px;padding:0 10px;line-height:20px}.ant-fullcalendar-date-panel{position:relative;outline:none}.ant-fullcalendar-calendar-body{padding:8px 12px}.ant-fullcalendar table{width:100%;max-width:100%;height:256px;background-color:transparent;border-collapse:collapse}.ant-fullcalendar table,.ant-fullcalendar td,.ant-fullcalendar th{border:0}.ant-fullcalendar td{position:relative}.ant-fullcalendar-calendar-table{margin-bottom:0;border-spacing:0}.ant-fullcalendar-column-header{width:33px;padding:0;line-height:18px;text-align:center}.ant-fullcalendar-column-header .ant-fullcalendar-column-header-inner{display:block;font-weight:400}.ant-fullcalendar-week-number-header .ant-fullcalendar-column-header-inner{display:none}.ant-fullcalendar-date,.ant-fullcalendar-month{text-align:center;-webkit-transition:all .3s;transition:all .3s}.ant-fullcalendar-value{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:24px;background:transparent;border-radius:2px;-webkit-transition:all .3s;transition:all .3s}.ant-fullcalendar-value:hover{background:#e6f7ff;cursor:pointer}.ant-fullcalendar-value:active{color:#fff;background:#1890ff}.ant-fullcalendar-month-panel-cell .ant-fullcalendar-value{width:48px}.ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-value,.ant-fullcalendar-today .ant-fullcalendar-value{-webkit-box-shadow:0 0 0 1px #1890ff inset;box-shadow:inset 0 0 0 1px #1890ff}.ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-value,.ant-fullcalendar-selected-day .ant-fullcalendar-value{color:#fff;background:#1890ff}.ant-fullcalendar-disabled-cell-first-of-row .ant-fullcalendar-value{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-fullcalendar-disabled-cell-last-of-row .ant-fullcalendar-value{border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-fullcalendar-last-month-cell .ant-fullcalendar-value,.ant-fullcalendar-next-month-btn-day .ant-fullcalendar-value{color:rgba(0,0,0,.25)}.ant-fullcalendar-month-panel-table{width:100%;table-layout:fixed;border-collapse:separate}.ant-fullcalendar-content{position:absolute;bottom:-9px;left:0;width:100%}.ant-fullcalendar-fullscreen{border-top:0}.ant-fullcalendar-fullscreen .ant-fullcalendar-table{table-layout:fixed}.ant-fullcalendar-fullscreen .ant-fullcalendar-header .ant-radio-group{margin-left:16px}.ant-fullcalendar-fullscreen .ant-fullcalendar-header label.ant-radio-button{height:32px;line-height:30px}.ant-fullcalendar-fullscreen .ant-fullcalendar-date,.ant-fullcalendar-fullscreen .ant-fullcalendar-month{display:block;height:116px;margin:0 4px;padding:4px 8px;color:rgba(0,0,0,.65);text-align:left;border-top:2px solid #e8e8e8;-webkit-transition:background .3s;transition:background .3s}.ant-fullcalendar-fullscreen .ant-fullcalendar-date:hover,.ant-fullcalendar-fullscreen .ant-fullcalendar-month:hover{background:#e6f7ff;cursor:pointer}.ant-fullcalendar-fullscreen .ant-fullcalendar-date:active,.ant-fullcalendar-fullscreen .ant-fullcalendar-month:active{background:#bae7ff}.ant-fullcalendar-fullscreen .ant-fullcalendar-column-header{padding-right:12px;padding-bottom:5px;text-align:right}.ant-fullcalendar-fullscreen .ant-fullcalendar-value{width:auto;text-align:right;background:transparent}.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-value{color:rgba(0,0,0,.65)}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-month,.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-date{background:transparent;border-top-color:#1890ff}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-current-cell .ant-fullcalendar-value,.ant-fullcalendar-fullscreen .ant-fullcalendar-today .ant-fullcalendar-value{-webkit-box-shadow:none;box-shadow:none}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-month,.ant-fullcalendar-fullscreen .ant-fullcalendar-selected-day .ant-fullcalendar-date{background:#e6f7ff}.ant-fullcalendar-fullscreen .ant-fullcalendar-month-panel-selected-cell .ant-fullcalendar-value,.ant-fullcalendar-fullscreen .ant-fullcalendar-selected-day .ant-fullcalendar-value{color:#1890ff}.ant-fullcalendar-fullscreen .ant-fullcalendar-last-month-cell .ant-fullcalendar-date,.ant-fullcalendar-fullscreen .ant-fullcalendar-next-month-btn-day .ant-fullcalendar-date{color:rgba(0,0,0,.25)}.ant-fullcalendar-fullscreen .ant-fullcalendar-content{position:static;width:auto;height:88px;overflow-y:auto}.ant-fullcalendar-disabled-cell .ant-fullcalendar-date,.ant-fullcalendar-disabled-cell .ant-fullcalendar-date:hover{cursor:not-allowed}.ant-fullcalendar-disabled-cell:not(.ant-fullcalendar-today) .ant-fullcalendar-date,.ant-fullcalendar-disabled-cell:not(.ant-fullcalendar-today) .ant-fullcalendar-date:hover{background:transparent}.ant-fullcalendar-disabled-cell .ant-fullcalendar-value{width:auto;color:rgba(0,0,0,.25);border-radius:0;cursor:not-allowed}.ant-radio-group{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block}.ant-radio-wrapper{margin:0 8px 0 0}.ant-radio,.ant-radio-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;white-space:nowrap;cursor:pointer}.ant-radio{margin:0;line-height:1;vertical-align:sub;outline:none}.ant-radio-input:focus+.ant-radio-inner,.ant-radio-wrapper:hover .ant-radio,.ant-radio:hover .ant-radio-inner{border-color:#1890ff}.ant-radio-input:focus+.ant-radio-inner{-webkit-box-shadow:0 0 0 3px rgba(24,144,255,.08);box-shadow:0 0 0 3px rgba(24,144,255,.08)}.ant-radio-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:50%;visibility:hidden;-webkit-animation:antRadioEffect .36s ease-in-out;animation:antRadioEffect .36s ease-in-out;-webkit-animation-fill-mode:both;animation-fill-mode:both;content:""}.ant-radio-wrapper:hover .ant-radio:after,.ant-radio:hover:after{visibility:visible}.ant-radio-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:100px;-webkit-transition:all .3s;transition:all .3s}.ant-radio-inner:after{position:absolute;top:3px;left:3px;display:table;width:8px;height:8px;background-color:#1890ff;border-top:0;border-left:0;border-radius:8px;-webkit-transform:scale(0);-ms-transform:scale(0);transform:scale(0);opacity:0;-webkit-transition:all .3s cubic-bezier(.78,.14,.15,.86);transition:all .3s cubic-bezier(.78,.14,.15,.86);content:" "}.ant-radio-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;cursor:pointer;opacity:0}.ant-radio-checked .ant-radio-inner{border-color:#1890ff}.ant-radio-checked .ant-radio-inner:after{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1);opacity:1;-webkit-transition:all .3s cubic-bezier(.78,.14,.15,.86);transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-radio-disabled .ant-radio-inner{background-color:#f5f5f5;border-color:#d9d9d9!important;cursor:not-allowed}.ant-radio-disabled .ant-radio-inner:after{background-color:rgba(0,0,0,.2)}.ant-radio-disabled .ant-radio-input{cursor:not-allowed}.ant-radio-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}span.ant-radio+*{padding-right:8px;padding-left:8px}.ant-radio-button-wrapper{position:relative;display:inline-block;height:32px;margin:0;padding:0 15px;color:rgba(0,0,0,.65);line-height:30px;background:#fff;border:1px solid #d9d9d9;border-top:1.02px solid #d9d9d9;border-left:0;cursor:pointer;-webkit-transition:color .3s,background .3s,border-color .3s;transition:color .3s,background .3s,border-color .3s}.ant-radio-button-wrapper a{color:rgba(0,0,0,.65)}.ant-radio-button-wrapper>.ant-radio-button{display:block;width:0;height:0;margin-left:0}.ant-radio-group-large .ant-radio-button-wrapper{height:40px;font-size:16px;line-height:38px}.ant-radio-group-small .ant-radio-button-wrapper{height:24px;padding:0 7px;line-height:22px}.ant-radio-button-wrapper:not(:first-child):before{position:absolute;top:0;left:-1px;display:block;width:1px;height:100%;background-color:#d9d9d9;content:""}.ant-radio-button-wrapper:first-child{border-left:1px solid #d9d9d9;border-radius:4px 0 0 4px}.ant-radio-button-wrapper:last-child{border-radius:0 4px 4px 0}.ant-radio-button-wrapper:first-child:last-child{border-radius:4px}.ant-radio-button-wrapper:hover{position:relative;color:#1890ff}.ant-radio-button-wrapper:focus-within{outline:3px solid rgba(24,144,255,.06)}.ant-radio-button-wrapper .ant-radio-inner,.ant-radio-button-wrapper input[type=checkbox],.ant-radio-button-wrapper input[type=radio]{width:0;height:0;opacity:0;pointer-events:none}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){z-index:1;color:#1890ff;background:#fff;border-color:#1890ff;-webkit-box-shadow:-1px 0 0 0 #1890ff;box-shadow:-1px 0 0 0 #1890ff}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):before{background-color:#1890ff!important;opacity:.1}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):first-child{border-color:#1890ff;-webkit-box-shadow:none!important;box-shadow:none!important}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#40a9ff;border-color:#40a9ff;-webkit-box-shadow:-1px 0 0 0 #40a9ff;box-shadow:-1px 0 0 0 #40a9ff}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#096dd9;border-color:#096dd9;-webkit-box-shadow:-1px 0 0 0 #096dd9;box-shadow:-1px 0 0 0 #096dd9}.ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled){color:#fff;background:#1890ff;border-color:#1890ff}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):hover{color:#fff;background:#40a9ff;border-color:#40a9ff}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):active{color:#fff;background:#096dd9;border-color:#096dd9}.ant-radio-group-solid .ant-radio-button-wrapper-checked:not(.ant-radio-button-wrapper-disabled):focus-within{outline:3px solid rgba(24,144,255,.06)}.ant-radio-button-wrapper-disabled{cursor:not-allowed}.ant-radio-button-wrapper-disabled,.ant-radio-button-wrapper-disabled:first-child,.ant-radio-button-wrapper-disabled:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9}.ant-radio-button-wrapper-disabled:first-child{border-left-color:#d9d9d9}.ant-radio-button-wrapper-disabled.ant-radio-button-wrapper-checked{color:#fff;background-color:#e6e6e6;border-color:#d9d9d9;-webkit-box-shadow:none;box-shadow:none}@-webkit-keyframes antRadioEffect{0%{-webkit-transform:scale(1);transform:scale(1);opacity:.5}to{-webkit-transform:scale(1.6);transform:scale(1.6);opacity:0}}@keyframes antRadioEffect{0%{-webkit-transform:scale(1);transform:scale(1);opacity:.5}to{-webkit-transform:scale(1.6);transform:scale(1.6);opacity:0}}@supports (-moz-appearance:meterbar) and (background-blend-mode:difference,normal){.ant-radio{vertical-align:text-bottom}}.ant-card{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;background:#fff;border-radius:2px;-webkit-transition:all .3s;transition:all .3s}.ant-card-hoverable{cursor:pointer}.ant-card-hoverable:hover{border-color:rgba(0,0,0,.09);-webkit-box-shadow:0 2px 8px rgba(0,0,0,.09);box-shadow:0 2px 8px rgba(0,0,0,.09)}.ant-card-bordered{border:1px solid #e8e8e8}.ant-card-head{min-height:48px;margin-bottom:-1px;padding:0 24px;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;background:transparent;border-bottom:1px solid #e8e8e8;border-radius:2px 2px 0 0;zoom:1}.ant-card-head:after,.ant-card-head:before{display:table;content:""}.ant-card-head:after{clear:both}.ant-card-head-wrapper{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.ant-card-head-title{display:inline-block;-ms-flex:1;flex:1 1;padding:16px 0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-card-head .ant-tabs{clear:both;margin-bottom:-17px;color:rgba(0,0,0,.65);font-weight:400;font-size:14px}.ant-card-head .ant-tabs-bar{border-bottom:1px solid #e8e8e8}.ant-card-extra{float:right;margin-left:auto;padding:16px 0;color:rgba(0,0,0,.65);font-weight:400;font-size:14px}.ant-card-body{padding:24px;zoom:1}.ant-card-body:after,.ant-card-body:before{display:table;content:""}.ant-card-body:after{clear:both}.ant-card-contain-grid:not(.ant-card-loading) .ant-card-body{margin:-1px 0 0 -1px;padding:0}.ant-card-grid{float:left;width:33.33%;padding:24px;border:0;border-radius:0;-webkit-box-shadow:1px 0 0 0 #e8e8e8,0 1px 0 0 #e8e8e8,1px 1px 0 0 #e8e8e8,1px 0 0 0 #e8e8e8 inset,0 1px 0 0 #e8e8e8 inset;box-shadow:1px 0 0 0 #e8e8e8,0 1px 0 0 #e8e8e8,1px 1px 0 0 #e8e8e8,inset 1px 0 0 0 #e8e8e8,inset 0 1px 0 0 #e8e8e8;-webkit-transition:all .3s;transition:all .3s}.ant-card-grid-hoverable:hover{position:relative;z-index:1;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-card-contain-tabs>.ant-card-head .ant-card-head-title{min-height:32px;padding-bottom:0}.ant-card-contain-tabs>.ant-card-head .ant-card-extra{padding-bottom:0}.ant-card-cover>*{display:block;width:100%}.ant-card-cover img{border-radius:2px 2px 0 0}.ant-card-actions{margin:0;padding:0;list-style:none;background:#fafafa;border-top:1px solid #e8e8e8;zoom:1}.ant-card-actions:after,.ant-card-actions:before{display:table;content:""}.ant-card-actions:after{clear:both}.ant-card-actions>li{float:left;margin:12px 0;color:rgba(0,0,0,.45);text-align:center}.ant-card-actions>li>span{position:relative;display:block;min-width:32px;font-size:14px;line-height:22px;cursor:pointer}.ant-card-actions>li>span:hover{color:#1890ff;-webkit-transition:color .3s;transition:color .3s}.ant-card-actions>li>span>.anticon,.ant-card-actions>li>span a:not(.ant-btn){display:inline-block;width:100%;color:rgba(0,0,0,.45);line-height:22px;-webkit-transition:color .3s;transition:color .3s}.ant-card-actions>li>span>.anticon:hover,.ant-card-actions>li>span a:not(.ant-btn):hover{color:#1890ff}.ant-card-actions>li>span>.anticon{font-size:16px;line-height:22px}.ant-card-actions>li:not(:last-child){border-right:1px solid #e8e8e8}.ant-card-type-inner .ant-card-head{padding:0 24px;background:#fafafa}.ant-card-type-inner .ant-card-head-title{padding:12px 0;font-size:14px}.ant-card-type-inner .ant-card-body{padding:16px 24px}.ant-card-type-inner .ant-card-extra{padding:13.5px 0}.ant-card-meta{margin:-4px 0;zoom:1}.ant-card-meta:after,.ant-card-meta:before{display:table;content:""}.ant-card-meta:after{clear:both}.ant-card-meta-avatar{float:left;padding-right:16px}.ant-card-meta-detail{overflow:hidden}.ant-card-meta-detail>div:not(:last-child){margin-bottom:8px}.ant-card-meta-title{overflow:hidden;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-overflow:ellipsis}.ant-card-meta-description{color:rgba(0,0,0,.45)}.ant-card-loading{overflow:hidden}.ant-card-loading .ant-card-body{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-card-loading-content p{margin:0}.ant-card-loading-block{height:14px;margin:4px 0;background:-webkit-gradient(linear,left top,right top,from(rgba(207,216,220,.2)),color-stop(rgba(207,216,220,.4)),to(rgba(207,216,220,.2)));background:linear-gradient(90deg,rgba(207,216,220,.2),rgba(207,216,220,.4),rgba(207,216,220,.2));background-size:600% 600%;border-radius:2px;-webkit-animation:card-loading 1.4s ease infinite;animation:card-loading 1.4s ease infinite}@-webkit-keyframes card-loading{0%,to{background-position:0 50%}50%{background-position:100% 50%}}@keyframes card-loading{0%,to{background-position:0 50%}50%{background-position:100% 50%}}.ant-card-small>.ant-card-head{min-height:36px;padding:0 12px;font-size:14px}.ant-card-small>.ant-card-head>.ant-card-head-wrapper>.ant-card-head-title{padding:8px 0}.ant-card-small>.ant-card-head>.ant-card-head-wrapper>.ant-card-extra{padding:8px 0;font-size:14px}.ant-card-small>.ant-card-body{padding:12px}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-container{height:40px}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-ink-bar{visibility:hidden}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab{height:40px;margin:0 2px 0 0;padding:0 16px;line-height:38px;background:#fafafa;border:1px solid #e8e8e8;border-radius:4px 4px 0 0;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active{height:40px;color:#1890ff;background:#fff;border-color:#e8e8e8;border-bottom:1px solid #fff}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-active:before{border-top:2px solid transparent}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-disabled{color:#1890ff;color:rgba(0,0,0,.25)}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab-inactive{padding:0}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-nav-wrap{margin-bottom:0}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x{width:16px;height:16px;height:14px;margin-right:-5px;margin-left:3px;overflow:hidden;color:rgba(0,0,0,.45);font-size:12px;vertical-align:middle;-webkit-transition:all .3s;transition:all .3s}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab .ant-tabs-close-x:hover{color:rgba(0,0,0,.85)}.ant-tabs.ant-tabs-card .ant-tabs-card-content>.ant-tabs-tabpane,.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content>.ant-tabs-tabpane{-webkit-transition:none!important;transition:none!important}.ant-tabs.ant-tabs-card .ant-tabs-card-content>.ant-tabs-tabpane-inactive,.ant-tabs.ant-tabs-editable-card .ant-tabs-card-content>.ant-tabs-tabpane-inactive{overflow:hidden}.ant-tabs.ant-tabs-card .ant-tabs-card-bar .ant-tabs-tab:hover .anticon-close{opacity:1}.ant-tabs-extra-content{line-height:45px}.ant-tabs-extra-content .ant-tabs-new-tab{position:relative;width:20px;height:20px;color:rgba(0,0,0,.65);font-size:12px;line-height:20px;text-align:center;border:1px solid #e8e8e8;border-radius:2px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-tabs-extra-content .ant-tabs-new-tab:hover{color:#1890ff;border-color:#1890ff}.ant-tabs-extra-content .ant-tabs-new-tab svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-tabs.ant-tabs-large .ant-tabs-extra-content{line-height:56px}.ant-tabs.ant-tabs-small .ant-tabs-extra-content{line-height:37px}.ant-tabs.ant-tabs-card .ant-tabs-extra-content{line-height:40px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-container{height:100%}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab{margin-bottom:8px;border-bottom:1px solid #e8e8e8}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{padding-bottom:4px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab:last-child,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab:last-child{margin-bottom:8px}.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-new-tab,.ant-tabs-vertical.ant-tabs-card .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-new-tab{width:90%}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-nav-wrap{margin-right:0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab{margin-right:1px;border-right:0;border-radius:4px 0 0 4px}.ant-tabs-vertical.ant-tabs-card.ant-tabs-left .ant-tabs-card-bar.ant-tabs-left-bar .ant-tabs-tab-active{margin-right:-1px;padding-right:18px}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-nav-wrap{margin-left:0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab{margin-left:1px;border-left:0;border-radius:0 4px 4px 0}.ant-tabs-vertical.ant-tabs-card.ant-tabs-right .ant-tabs-card-bar.ant-tabs-right-bar .ant-tabs-tab-active{margin-left:-1px;padding-left:18px}.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab{height:auto;border-top:0;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}.ant-tabs .ant-tabs-card-bar.ant-tabs-bottom-bar .ant-tabs-tab-active{padding-top:1px;padding-bottom:0;color:#1890ff}.ant-tabs{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;overflow:hidden;zoom:1}.ant-tabs:after,.ant-tabs:before{display:table;content:""}.ant-tabs:after{clear:both}.ant-tabs-ink-bar{position:absolute;bottom:1px;left:0;z-index:1;-webkit-box-sizing:border-box;box-sizing:border-box;width:0;height:2px;background-color:#1890ff;-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.ant-tabs-bar{margin:0 0 16px;border-bottom:1px solid #e8e8e8;outline:none}.ant-tabs-bar,.ant-tabs-nav-container{-webkit-transition:padding .3s cubic-bezier(.645,.045,.355,1);transition:padding .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav-container{position:relative;-webkit-box-sizing:border-box;box-sizing:border-box;margin-bottom:-1px;overflow:hidden;font-size:14px;line-height:1.5;white-space:nowrap;zoom:1}.ant-tabs-nav-container:after,.ant-tabs-nav-container:before{display:table;content:""}.ant-tabs-nav-container:after{clear:both}.ant-tabs-nav-container-scrolling{padding-right:32px;padding-left:32px}.ant-tabs-bottom .ant-tabs-bottom-bar{margin-top:16px;margin-bottom:0;border-top:1px solid #e8e8e8;border-bottom:none}.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-ink-bar{top:1px;bottom:auto}.ant-tabs-bottom .ant-tabs-bottom-bar .ant-tabs-nav-container{margin-top:-1px;margin-bottom:0}.ant-tabs-tab-next,.ant-tabs-tab-prev{position:absolute;z-index:2;width:0;height:100%;color:rgba(0,0,0,.45);text-align:center;background-color:transparent;border:0;cursor:pointer;opacity:0;-webkit-transition:width .3s cubic-bezier(.645,.045,.355,1),opacity .3s cubic-bezier(.645,.045,.355,1),color .3s cubic-bezier(.645,.045,.355,1);transition:width .3s cubic-bezier(.645,.045,.355,1),opacity .3s cubic-bezier(.645,.045,.355,1),color .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.ant-tabs-tab-next.ant-tabs-tab-arrow-show,.ant-tabs-tab-prev.ant-tabs-tab-arrow-show{width:32px;height:100%;opacity:1;pointer-events:auto}.ant-tabs-tab-next:hover,.ant-tabs-tab-prev:hover{color:rgba(0,0,0,.65)}.ant-tabs-tab-next-icon,.ant-tabs-tab-prev-icon{position:absolute;top:50%;left:50%;font-weight:700;font-style:normal;-webkit-font-feature-settings:normal;font-feature-settings:normal;font-variant:normal;line-height:inherit;text-align:center;text-transform:none;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ant-tabs-tab-next-icon-target,.ant-tabs-tab-prev-icon-target{display:block;display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg)}:root .ant-tabs-tab-next-icon-target,:root .ant-tabs-tab-prev-icon-target{font-size:12px}.ant-tabs-tab-btn-disabled{cursor:not-allowed}.ant-tabs-tab-btn-disabled,.ant-tabs-tab-btn-disabled:hover{color:rgba(0,0,0,.25)}.ant-tabs-tab-next{right:2px}.ant-tabs-tab-prev{left:0}:root .ant-tabs-tab-prev{-webkit-filter:none;filter:none}.ant-tabs-nav-wrap{margin-bottom:-1px;overflow:hidden}.ant-tabs-nav-scroll{overflow:hidden;white-space:nowrap}.ant-tabs-nav{position:relative;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding-left:0;list-style:none;-webkit-transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav:after,.ant-tabs-nav:before{display:table;content:" "}.ant-tabs-nav:after{clear:both}.ant-tabs-nav .ant-tabs-tab{position:relative;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;height:100%;margin:0 32px 0 0;padding:12px 16px;text-decoration:none;cursor:pointer;-webkit-transition:color .3s cubic-bezier(.645,.045,.355,1);transition:color .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-nav .ant-tabs-tab:before{position:absolute;top:-1px;left:0;width:100%;border-top:2px solid transparent;border-radius:4px 4px 0 0;-webkit-transition:all .3s;transition:all .3s;content:"";pointer-events:none}.ant-tabs-nav .ant-tabs-tab:last-child{margin-right:0}.ant-tabs-nav .ant-tabs-tab:hover{color:#40a9ff}.ant-tabs-nav .ant-tabs-tab:active{color:#096dd9}.ant-tabs-nav .ant-tabs-tab .anticon{margin-right:8px}.ant-tabs-nav .ant-tabs-tab-active{color:#1890ff;font-weight:500}.ant-tabs-nav .ant-tabs-tab-disabled,.ant-tabs-nav .ant-tabs-tab-disabled:hover{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-tabs .ant-tabs-large-bar .ant-tabs-nav-container{font-size:16px}.ant-tabs .ant-tabs-large-bar .ant-tabs-tab{padding:16px}.ant-tabs .ant-tabs-small-bar .ant-tabs-nav-container{font-size:14px}.ant-tabs .ant-tabs-small-bar .ant-tabs-tab{padding:8px 16px}.ant-tabs-content:before{display:block;overflow:hidden;content:""}.ant-tabs .ant-tabs-bottom-content,.ant-tabs .ant-tabs-top-content{width:100%}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane{-ms-flex-negative:0;flex-shrink:0;width:100%;-webkit-backface-visibility:hidden;opacity:1;-webkit-transition:opacity .45s;transition:opacity .45s}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane-inactive,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs .ant-tabs-bottom-content>.ant-tabs-tabpane-inactive input,.ant-tabs .ant-tabs-top-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-tabs .ant-tabs-bottom-content.ant-tabs-content-animated,.ant-tabs .ant-tabs-top-content.ant-tabs-content-animated{display:-ms-flexbox;display:flex;-ms-flex-direction:row;flex-direction:row;-webkit-transition:margin-left .3s cubic-bezier(.645,.045,.355,1);transition:margin-left .3s cubic-bezier(.645,.045,.355,1);will-change:margin-left}.ant-tabs .ant-tabs-left-bar,.ant-tabs .ant-tabs-right-bar{height:100%;border-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-arrow-show,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-arrow-show{width:100%;height:32px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab{display:block;float:none;margin:0 0 16px;padding:8px 24px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab:last-child,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab:last-child{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-extra-content,.ant-tabs .ant-tabs-right-bar .ant-tabs-extra-content{text-align:center}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-scroll,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-scroll{width:auto}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{height:100%}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container.ant-tabs-nav-container-scrolling{padding:32px 0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{margin-bottom:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav{width:100%}.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar,.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar{top:0;bottom:auto;left:auto;width:2px;height:0}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-next,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-next{right:0;bottom:0;width:100%;height:32px}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab-prev,.ant-tabs .ant-tabs-right-bar .ant-tabs-tab-prev{top:0;width:100%;height:32px}.ant-tabs .ant-tabs-left-content,.ant-tabs .ant-tabs-right-content{width:auto;margin-top:0!important;overflow:hidden}.ant-tabs .ant-tabs-left-bar{float:left;margin-right:-1px;margin-bottom:0;border-right:1px solid #e8e8e8}.ant-tabs .ant-tabs-left-bar .ant-tabs-tab{text-align:right}.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-left-bar .ant-tabs-nav-wrap{margin-right:-1px}.ant-tabs .ant-tabs-left-bar .ant-tabs-ink-bar{right:1px}.ant-tabs .ant-tabs-left-content{padding-left:24px;border-left:1px solid #e8e8e8}.ant-tabs .ant-tabs-right-bar{float:right;margin-bottom:0;margin-left:-1px;border-left:1px solid #e8e8e8}.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-container,.ant-tabs .ant-tabs-right-bar .ant-tabs-nav-wrap{margin-left:-1px}.ant-tabs .ant-tabs-right-bar .ant-tabs-ink-bar{left:1px}.ant-tabs .ant-tabs-right-content{padding-right:24px;border-right:1px solid #e8e8e8}.ant-tabs-bottom .ant-tabs-ink-bar-animated,.ant-tabs-top .ant-tabs-ink-bar-animated{-webkit-transition:width .2s cubic-bezier(.645,.045,.355,1),left .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:width .2s cubic-bezier(.645,.045,.355,1),left .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),width .2s cubic-bezier(.645,.045,.355,1),left .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),width .2s cubic-bezier(.645,.045,.355,1),left .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-left .ant-tabs-ink-bar-animated,.ant-tabs-right .ant-tabs-ink-bar-animated{-webkit-transition:height .2s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:height .2s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),height .2s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1);transition:transform .3s cubic-bezier(.645,.045,.355,1),height .2s cubic-bezier(.645,.045,.355,1),top .3s cubic-bezier(.645,.045,.355,1),-webkit-transform .3s cubic-bezier(.645,.045,.355,1)}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-content-animated,.no-flex>.ant-tabs-content>.ant-tabs-content-animated{margin-left:0!important;-webkit-transform:none!important;-ms-transform:none!important;transform:none!important}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-tabpane-inactive,.no-flex>.ant-tabs-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs-no-animation>.ant-tabs-content>.ant-tabs-tabpane-inactive input,.no-flex>.ant-tabs-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-tabs-left-content>.ant-tabs-content-animated,.ant-tabs-right-content>.ant-tabs-content-animated{margin-left:0!important;-webkit-transform:none!important;-ms-transform:none!important;transform:none!important}.ant-tabs-left-content>.ant-tabs-tabpane-inactive,.ant-tabs-right-content>.ant-tabs-tabpane-inactive{height:0;padding:0!important;overflow:hidden;opacity:0;pointer-events:none}.ant-tabs-left-content>.ant-tabs-tabpane-inactive input,.ant-tabs-right-content>.ant-tabs-tabpane-inactive input{visibility:hidden}.ant-row{position:relative;height:auto;margin-right:0;margin-left:0;zoom:1;display:block;-webkit-box-sizing:border-box;box-sizing:border-box}.ant-row:after,.ant-row:before{display:table;content:""}.ant-row+.ant-row:before,.ant-row:after{clear:both}.ant-row-flex{-ms-flex-flow:row wrap;flex-flow:row wrap}.ant-row-flex,.ant-row-flex:after,.ant-row-flex:before{display:-ms-flexbox;display:flex}.ant-row-flex-start{-ms-flex-pack:start;justify-content:flex-start}.ant-row-flex-center{-ms-flex-pack:center;justify-content:center}.ant-row-flex-end{-ms-flex-pack:end;justify-content:flex-end}.ant-row-flex-space-between{-ms-flex-pack:justify;justify-content:space-between}.ant-row-flex-space-around{-ms-flex-pack:distribute;justify-content:space-around}.ant-row-flex-top{-ms-flex-align:start;align-items:flex-start}.ant-row-flex-middle{-ms-flex-align:center;align-items:center}.ant-row-flex-bottom{-ms-flex-align:end;align-items:flex-end}.ant-col{position:relative;min-height:1px}.ant-col-1,.ant-col-2,.ant-col-3,.ant-col-4,.ant-col-5,.ant-col-6,.ant-col-7,.ant-col-8,.ant-col-9,.ant-col-10,.ant-col-11,.ant-col-12,.ant-col-13,.ant-col-14,.ant-col-15,.ant-col-16,.ant-col-17,.ant-col-18,.ant-col-19,.ant-col-20,.ant-col-21,.ant-col-22,.ant-col-23,.ant-col-24,.ant-col-lg-1,.ant-col-lg-2,.ant-col-lg-3,.ant-col-lg-4,.ant-col-lg-5,.ant-col-lg-6,.ant-col-lg-7,.ant-col-lg-8,.ant-col-lg-9,.ant-col-lg-10,.ant-col-lg-11,.ant-col-lg-12,.ant-col-lg-13,.ant-col-lg-14,.ant-col-lg-15,.ant-col-lg-16,.ant-col-lg-17,.ant-col-lg-18,.ant-col-lg-19,.ant-col-lg-20,.ant-col-lg-21,.ant-col-lg-22,.ant-col-lg-23,.ant-col-lg-24,.ant-col-md-1,.ant-col-md-2,.ant-col-md-3,.ant-col-md-4,.ant-col-md-5,.ant-col-md-6,.ant-col-md-7,.ant-col-md-8,.ant-col-md-9,.ant-col-md-10,.ant-col-md-11,.ant-col-md-12,.ant-col-md-13,.ant-col-md-14,.ant-col-md-15,.ant-col-md-16,.ant-col-md-17,.ant-col-md-18,.ant-col-md-19,.ant-col-md-20,.ant-col-md-21,.ant-col-md-22,.ant-col-md-23,.ant-col-md-24,.ant-col-sm-1,.ant-col-sm-2,.ant-col-sm-3,.ant-col-sm-4,.ant-col-sm-5,.ant-col-sm-6,.ant-col-sm-7,.ant-col-sm-8,.ant-col-sm-9,.ant-col-sm-10,.ant-col-sm-11,.ant-col-sm-12,.ant-col-sm-13,.ant-col-sm-14,.ant-col-sm-15,.ant-col-sm-16,.ant-col-sm-17,.ant-col-sm-18,.ant-col-sm-19,.ant-col-sm-20,.ant-col-sm-21,.ant-col-sm-22,.ant-col-sm-23,.ant-col-sm-24,.ant-col-xs-1,.ant-col-xs-2,.ant-col-xs-3,.ant-col-xs-4,.ant-col-xs-5,.ant-col-xs-6,.ant-col-xs-7,.ant-col-xs-8,.ant-col-xs-9,.ant-col-xs-10,.ant-col-xs-11,.ant-col-xs-12,.ant-col-xs-13,.ant-col-xs-14,.ant-col-xs-15,.ant-col-xs-16,.ant-col-xs-17,.ant-col-xs-18,.ant-col-xs-19,.ant-col-xs-20,.ant-col-xs-21,.ant-col-xs-22,.ant-col-xs-23,.ant-col-xs-24{position:relative;padding-right:0;padding-left:0}.ant-col-1,.ant-col-2,.ant-col-3,.ant-col-4,.ant-col-5,.ant-col-6,.ant-col-7,.ant-col-8,.ant-col-9,.ant-col-10,.ant-col-11,.ant-col-12,.ant-col-13,.ant-col-14,.ant-col-15,.ant-col-16,.ant-col-17,.ant-col-18,.ant-col-19,.ant-col-20,.ant-col-21,.ant-col-22,.ant-col-23,.ant-col-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-push-24{left:100%}.ant-col-pull-24{right:100%}.ant-col-offset-24{margin-left:100%}.ant-col-order-24{-ms-flex-order:24;order:24}.ant-col-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-push-23{left:95.83333333%}.ant-col-pull-23{right:95.83333333%}.ant-col-offset-23{margin-left:95.83333333%}.ant-col-order-23{-ms-flex-order:23;order:23}.ant-col-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-push-22{left:91.66666667%}.ant-col-pull-22{right:91.66666667%}.ant-col-offset-22{margin-left:91.66666667%}.ant-col-order-22{-ms-flex-order:22;order:22}.ant-col-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-push-21{left:87.5%}.ant-col-pull-21{right:87.5%}.ant-col-offset-21{margin-left:87.5%}.ant-col-order-21{-ms-flex-order:21;order:21}.ant-col-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-push-20{left:83.33333333%}.ant-col-pull-20{right:83.33333333%}.ant-col-offset-20{margin-left:83.33333333%}.ant-col-order-20{-ms-flex-order:20;order:20}.ant-col-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-push-19{left:79.16666667%}.ant-col-pull-19{right:79.16666667%}.ant-col-offset-19{margin-left:79.16666667%}.ant-col-order-19{-ms-flex-order:19;order:19}.ant-col-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-push-18{left:75%}.ant-col-pull-18{right:75%}.ant-col-offset-18{margin-left:75%}.ant-col-order-18{-ms-flex-order:18;order:18}.ant-col-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-push-17{left:70.83333333%}.ant-col-pull-17{right:70.83333333%}.ant-col-offset-17{margin-left:70.83333333%}.ant-col-order-17{-ms-flex-order:17;order:17}.ant-col-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-push-16{left:66.66666667%}.ant-col-pull-16{right:66.66666667%}.ant-col-offset-16{margin-left:66.66666667%}.ant-col-order-16{-ms-flex-order:16;order:16}.ant-col-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-push-15{left:62.5%}.ant-col-pull-15{right:62.5%}.ant-col-offset-15{margin-left:62.5%}.ant-col-order-15{-ms-flex-order:15;order:15}.ant-col-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-push-14{left:58.33333333%}.ant-col-pull-14{right:58.33333333%}.ant-col-offset-14{margin-left:58.33333333%}.ant-col-order-14{-ms-flex-order:14;order:14}.ant-col-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-push-13{left:54.16666667%}.ant-col-pull-13{right:54.16666667%}.ant-col-offset-13{margin-left:54.16666667%}.ant-col-order-13{-ms-flex-order:13;order:13}.ant-col-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-push-12{left:50%}.ant-col-pull-12{right:50%}.ant-col-offset-12{margin-left:50%}.ant-col-order-12{-ms-flex-order:12;order:12}.ant-col-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-push-11{left:45.83333333%}.ant-col-pull-11{right:45.83333333%}.ant-col-offset-11{margin-left:45.83333333%}.ant-col-order-11{-ms-flex-order:11;order:11}.ant-col-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-push-10{left:41.66666667%}.ant-col-pull-10{right:41.66666667%}.ant-col-offset-10{margin-left:41.66666667%}.ant-col-order-10{-ms-flex-order:10;order:10}.ant-col-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-push-9{left:37.5%}.ant-col-pull-9{right:37.5%}.ant-col-offset-9{margin-left:37.5%}.ant-col-order-9{-ms-flex-order:9;order:9}.ant-col-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-push-8{left:33.33333333%}.ant-col-pull-8{right:33.33333333%}.ant-col-offset-8{margin-left:33.33333333%}.ant-col-order-8{-ms-flex-order:8;order:8}.ant-col-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-push-7{left:29.16666667%}.ant-col-pull-7{right:29.16666667%}.ant-col-offset-7{margin-left:29.16666667%}.ant-col-order-7{-ms-flex-order:7;order:7}.ant-col-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-push-6{left:25%}.ant-col-pull-6{right:25%}.ant-col-offset-6{margin-left:25%}.ant-col-order-6{-ms-flex-order:6;order:6}.ant-col-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-push-5{left:20.83333333%}.ant-col-pull-5{right:20.83333333%}.ant-col-offset-5{margin-left:20.83333333%}.ant-col-order-5{-ms-flex-order:5;order:5}.ant-col-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-push-4{left:16.66666667%}.ant-col-pull-4{right:16.66666667%}.ant-col-offset-4{margin-left:16.66666667%}.ant-col-order-4{-ms-flex-order:4;order:4}.ant-col-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-push-3{left:12.5%}.ant-col-pull-3{right:12.5%}.ant-col-offset-3{margin-left:12.5%}.ant-col-order-3{-ms-flex-order:3;order:3}.ant-col-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-push-2{left:8.33333333%}.ant-col-pull-2{right:8.33333333%}.ant-col-offset-2{margin-left:8.33333333%}.ant-col-order-2{-ms-flex-order:2;order:2}.ant-col-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-push-1{left:4.16666667%}.ant-col-pull-1{right:4.16666667%}.ant-col-offset-1{margin-left:4.16666667%}.ant-col-order-1{-ms-flex-order:1;order:1}.ant-col-0{display:none}.ant-col-offset-0{margin-left:0}.ant-col-order-0{-ms-flex-order:0;order:0}.ant-col-xs-1,.ant-col-xs-2,.ant-col-xs-3,.ant-col-xs-4,.ant-col-xs-5,.ant-col-xs-6,.ant-col-xs-7,.ant-col-xs-8,.ant-col-xs-9,.ant-col-xs-10,.ant-col-xs-11,.ant-col-xs-12,.ant-col-xs-13,.ant-col-xs-14,.ant-col-xs-15,.ant-col-xs-16,.ant-col-xs-17,.ant-col-xs-18,.ant-col-xs-19,.ant-col-xs-20,.ant-col-xs-21,.ant-col-xs-22,.ant-col-xs-23,.ant-col-xs-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-xs-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-xs-push-24{left:100%}.ant-col-xs-pull-24{right:100%}.ant-col-xs-offset-24{margin-left:100%}.ant-col-xs-order-24{-ms-flex-order:24;order:24}.ant-col-xs-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-xs-push-23{left:95.83333333%}.ant-col-xs-pull-23{right:95.83333333%}.ant-col-xs-offset-23{margin-left:95.83333333%}.ant-col-xs-order-23{-ms-flex-order:23;order:23}.ant-col-xs-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-xs-push-22{left:91.66666667%}.ant-col-xs-pull-22{right:91.66666667%}.ant-col-xs-offset-22{margin-left:91.66666667%}.ant-col-xs-order-22{-ms-flex-order:22;order:22}.ant-col-xs-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-xs-push-21{left:87.5%}.ant-col-xs-pull-21{right:87.5%}.ant-col-xs-offset-21{margin-left:87.5%}.ant-col-xs-order-21{-ms-flex-order:21;order:21}.ant-col-xs-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-xs-push-20{left:83.33333333%}.ant-col-xs-pull-20{right:83.33333333%}.ant-col-xs-offset-20{margin-left:83.33333333%}.ant-col-xs-order-20{-ms-flex-order:20;order:20}.ant-col-xs-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-xs-push-19{left:79.16666667%}.ant-col-xs-pull-19{right:79.16666667%}.ant-col-xs-offset-19{margin-left:79.16666667%}.ant-col-xs-order-19{-ms-flex-order:19;order:19}.ant-col-xs-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-xs-push-18{left:75%}.ant-col-xs-pull-18{right:75%}.ant-col-xs-offset-18{margin-left:75%}.ant-col-xs-order-18{-ms-flex-order:18;order:18}.ant-col-xs-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-xs-push-17{left:70.83333333%}.ant-col-xs-pull-17{right:70.83333333%}.ant-col-xs-offset-17{margin-left:70.83333333%}.ant-col-xs-order-17{-ms-flex-order:17;order:17}.ant-col-xs-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-xs-push-16{left:66.66666667%}.ant-col-xs-pull-16{right:66.66666667%}.ant-col-xs-offset-16{margin-left:66.66666667%}.ant-col-xs-order-16{-ms-flex-order:16;order:16}.ant-col-xs-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-xs-push-15{left:62.5%}.ant-col-xs-pull-15{right:62.5%}.ant-col-xs-offset-15{margin-left:62.5%}.ant-col-xs-order-15{-ms-flex-order:15;order:15}.ant-col-xs-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-xs-push-14{left:58.33333333%}.ant-col-xs-pull-14{right:58.33333333%}.ant-col-xs-offset-14{margin-left:58.33333333%}.ant-col-xs-order-14{-ms-flex-order:14;order:14}.ant-col-xs-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-xs-push-13{left:54.16666667%}.ant-col-xs-pull-13{right:54.16666667%}.ant-col-xs-offset-13{margin-left:54.16666667%}.ant-col-xs-order-13{-ms-flex-order:13;order:13}.ant-col-xs-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-xs-push-12{left:50%}.ant-col-xs-pull-12{right:50%}.ant-col-xs-offset-12{margin-left:50%}.ant-col-xs-order-12{-ms-flex-order:12;order:12}.ant-col-xs-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-xs-push-11{left:45.83333333%}.ant-col-xs-pull-11{right:45.83333333%}.ant-col-xs-offset-11{margin-left:45.83333333%}.ant-col-xs-order-11{-ms-flex-order:11;order:11}.ant-col-xs-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-xs-push-10{left:41.66666667%}.ant-col-xs-pull-10{right:41.66666667%}.ant-col-xs-offset-10{margin-left:41.66666667%}.ant-col-xs-order-10{-ms-flex-order:10;order:10}.ant-col-xs-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-xs-push-9{left:37.5%}.ant-col-xs-pull-9{right:37.5%}.ant-col-xs-offset-9{margin-left:37.5%}.ant-col-xs-order-9{-ms-flex-order:9;order:9}.ant-col-xs-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-xs-push-8{left:33.33333333%}.ant-col-xs-pull-8{right:33.33333333%}.ant-col-xs-offset-8{margin-left:33.33333333%}.ant-col-xs-order-8{-ms-flex-order:8;order:8}.ant-col-xs-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-xs-push-7{left:29.16666667%}.ant-col-xs-pull-7{right:29.16666667%}.ant-col-xs-offset-7{margin-left:29.16666667%}.ant-col-xs-order-7{-ms-flex-order:7;order:7}.ant-col-xs-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-xs-push-6{left:25%}.ant-col-xs-pull-6{right:25%}.ant-col-xs-offset-6{margin-left:25%}.ant-col-xs-order-6{-ms-flex-order:6;order:6}.ant-col-xs-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-xs-push-5{left:20.83333333%}.ant-col-xs-pull-5{right:20.83333333%}.ant-col-xs-offset-5{margin-left:20.83333333%}.ant-col-xs-order-5{-ms-flex-order:5;order:5}.ant-col-xs-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-xs-push-4{left:16.66666667%}.ant-col-xs-pull-4{right:16.66666667%}.ant-col-xs-offset-4{margin-left:16.66666667%}.ant-col-xs-order-4{-ms-flex-order:4;order:4}.ant-col-xs-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-xs-push-3{left:12.5%}.ant-col-xs-pull-3{right:12.5%}.ant-col-xs-offset-3{margin-left:12.5%}.ant-col-xs-order-3{-ms-flex-order:3;order:3}.ant-col-xs-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-xs-push-2{left:8.33333333%}.ant-col-xs-pull-2{right:8.33333333%}.ant-col-xs-offset-2{margin-left:8.33333333%}.ant-col-xs-order-2{-ms-flex-order:2;order:2}.ant-col-xs-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-xs-push-1{left:4.16666667%}.ant-col-xs-pull-1{right:4.16666667%}.ant-col-xs-offset-1{margin-left:4.16666667%}.ant-col-xs-order-1{-ms-flex-order:1;order:1}.ant-col-xs-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xs-push-0{left:auto}.ant-col-xs-pull-0{right:auto}.ant-col-xs-offset-0{margin-left:0}.ant-col-xs-order-0{-ms-flex-order:0;order:0}@media (min-width:576px){.ant-col-sm-1,.ant-col-sm-2,.ant-col-sm-3,.ant-col-sm-4,.ant-col-sm-5,.ant-col-sm-6,.ant-col-sm-7,.ant-col-sm-8,.ant-col-sm-9,.ant-col-sm-10,.ant-col-sm-11,.ant-col-sm-12,.ant-col-sm-13,.ant-col-sm-14,.ant-col-sm-15,.ant-col-sm-16,.ant-col-sm-17,.ant-col-sm-18,.ant-col-sm-19,.ant-col-sm-20,.ant-col-sm-21,.ant-col-sm-22,.ant-col-sm-23,.ant-col-sm-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-sm-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-sm-push-24{left:100%}.ant-col-sm-pull-24{right:100%}.ant-col-sm-offset-24{margin-left:100%}.ant-col-sm-order-24{-ms-flex-order:24;order:24}.ant-col-sm-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-sm-push-23{left:95.83333333%}.ant-col-sm-pull-23{right:95.83333333%}.ant-col-sm-offset-23{margin-left:95.83333333%}.ant-col-sm-order-23{-ms-flex-order:23;order:23}.ant-col-sm-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-sm-push-22{left:91.66666667%}.ant-col-sm-pull-22{right:91.66666667%}.ant-col-sm-offset-22{margin-left:91.66666667%}.ant-col-sm-order-22{-ms-flex-order:22;order:22}.ant-col-sm-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-sm-push-21{left:87.5%}.ant-col-sm-pull-21{right:87.5%}.ant-col-sm-offset-21{margin-left:87.5%}.ant-col-sm-order-21{-ms-flex-order:21;order:21}.ant-col-sm-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-sm-push-20{left:83.33333333%}.ant-col-sm-pull-20{right:83.33333333%}.ant-col-sm-offset-20{margin-left:83.33333333%}.ant-col-sm-order-20{-ms-flex-order:20;order:20}.ant-col-sm-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-sm-push-19{left:79.16666667%}.ant-col-sm-pull-19{right:79.16666667%}.ant-col-sm-offset-19{margin-left:79.16666667%}.ant-col-sm-order-19{-ms-flex-order:19;order:19}.ant-col-sm-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-sm-push-18{left:75%}.ant-col-sm-pull-18{right:75%}.ant-col-sm-offset-18{margin-left:75%}.ant-col-sm-order-18{-ms-flex-order:18;order:18}.ant-col-sm-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-sm-push-17{left:70.83333333%}.ant-col-sm-pull-17{right:70.83333333%}.ant-col-sm-offset-17{margin-left:70.83333333%}.ant-col-sm-order-17{-ms-flex-order:17;order:17}.ant-col-sm-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-sm-push-16{left:66.66666667%}.ant-col-sm-pull-16{right:66.66666667%}.ant-col-sm-offset-16{margin-left:66.66666667%}.ant-col-sm-order-16{-ms-flex-order:16;order:16}.ant-col-sm-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-sm-push-15{left:62.5%}.ant-col-sm-pull-15{right:62.5%}.ant-col-sm-offset-15{margin-left:62.5%}.ant-col-sm-order-15{-ms-flex-order:15;order:15}.ant-col-sm-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-sm-push-14{left:58.33333333%}.ant-col-sm-pull-14{right:58.33333333%}.ant-col-sm-offset-14{margin-left:58.33333333%}.ant-col-sm-order-14{-ms-flex-order:14;order:14}.ant-col-sm-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-sm-push-13{left:54.16666667%}.ant-col-sm-pull-13{right:54.16666667%}.ant-col-sm-offset-13{margin-left:54.16666667%}.ant-col-sm-order-13{-ms-flex-order:13;order:13}.ant-col-sm-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-sm-push-12{left:50%}.ant-col-sm-pull-12{right:50%}.ant-col-sm-offset-12{margin-left:50%}.ant-col-sm-order-12{-ms-flex-order:12;order:12}.ant-col-sm-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-sm-push-11{left:45.83333333%}.ant-col-sm-pull-11{right:45.83333333%}.ant-col-sm-offset-11{margin-left:45.83333333%}.ant-col-sm-order-11{-ms-flex-order:11;order:11}.ant-col-sm-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-sm-push-10{left:41.66666667%}.ant-col-sm-pull-10{right:41.66666667%}.ant-col-sm-offset-10{margin-left:41.66666667%}.ant-col-sm-order-10{-ms-flex-order:10;order:10}.ant-col-sm-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-sm-push-9{left:37.5%}.ant-col-sm-pull-9{right:37.5%}.ant-col-sm-offset-9{margin-left:37.5%}.ant-col-sm-order-9{-ms-flex-order:9;order:9}.ant-col-sm-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-sm-push-8{left:33.33333333%}.ant-col-sm-pull-8{right:33.33333333%}.ant-col-sm-offset-8{margin-left:33.33333333%}.ant-col-sm-order-8{-ms-flex-order:8;order:8}.ant-col-sm-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-sm-push-7{left:29.16666667%}.ant-col-sm-pull-7{right:29.16666667%}.ant-col-sm-offset-7{margin-left:29.16666667%}.ant-col-sm-order-7{-ms-flex-order:7;order:7}.ant-col-sm-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-sm-push-6{left:25%}.ant-col-sm-pull-6{right:25%}.ant-col-sm-offset-6{margin-left:25%}.ant-col-sm-order-6{-ms-flex-order:6;order:6}.ant-col-sm-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-sm-push-5{left:20.83333333%}.ant-col-sm-pull-5{right:20.83333333%}.ant-col-sm-offset-5{margin-left:20.83333333%}.ant-col-sm-order-5{-ms-flex-order:5;order:5}.ant-col-sm-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-sm-push-4{left:16.66666667%}.ant-col-sm-pull-4{right:16.66666667%}.ant-col-sm-offset-4{margin-left:16.66666667%}.ant-col-sm-order-4{-ms-flex-order:4;order:4}.ant-col-sm-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-sm-push-3{left:12.5%}.ant-col-sm-pull-3{right:12.5%}.ant-col-sm-offset-3{margin-left:12.5%}.ant-col-sm-order-3{-ms-flex-order:3;order:3}.ant-col-sm-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-sm-push-2{left:8.33333333%}.ant-col-sm-pull-2{right:8.33333333%}.ant-col-sm-offset-2{margin-left:8.33333333%}.ant-col-sm-order-2{-ms-flex-order:2;order:2}.ant-col-sm-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-sm-push-1{left:4.16666667%}.ant-col-sm-pull-1{right:4.16666667%}.ant-col-sm-offset-1{margin-left:4.16666667%}.ant-col-sm-order-1{-ms-flex-order:1;order:1}.ant-col-sm-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-sm-push-0{left:auto}.ant-col-sm-pull-0{right:auto}.ant-col-sm-offset-0{margin-left:0}.ant-col-sm-order-0{-ms-flex-order:0;order:0}}@media (min-width:768px){.ant-col-md-1,.ant-col-md-2,.ant-col-md-3,.ant-col-md-4,.ant-col-md-5,.ant-col-md-6,.ant-col-md-7,.ant-col-md-8,.ant-col-md-9,.ant-col-md-10,.ant-col-md-11,.ant-col-md-12,.ant-col-md-13,.ant-col-md-14,.ant-col-md-15,.ant-col-md-16,.ant-col-md-17,.ant-col-md-18,.ant-col-md-19,.ant-col-md-20,.ant-col-md-21,.ant-col-md-22,.ant-col-md-23,.ant-col-md-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-md-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-md-push-24{left:100%}.ant-col-md-pull-24{right:100%}.ant-col-md-offset-24{margin-left:100%}.ant-col-md-order-24{-ms-flex-order:24;order:24}.ant-col-md-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-md-push-23{left:95.83333333%}.ant-col-md-pull-23{right:95.83333333%}.ant-col-md-offset-23{margin-left:95.83333333%}.ant-col-md-order-23{-ms-flex-order:23;order:23}.ant-col-md-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-md-push-22{left:91.66666667%}.ant-col-md-pull-22{right:91.66666667%}.ant-col-md-offset-22{margin-left:91.66666667%}.ant-col-md-order-22{-ms-flex-order:22;order:22}.ant-col-md-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-md-push-21{left:87.5%}.ant-col-md-pull-21{right:87.5%}.ant-col-md-offset-21{margin-left:87.5%}.ant-col-md-order-21{-ms-flex-order:21;order:21}.ant-col-md-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-md-push-20{left:83.33333333%}.ant-col-md-pull-20{right:83.33333333%}.ant-col-md-offset-20{margin-left:83.33333333%}.ant-col-md-order-20{-ms-flex-order:20;order:20}.ant-col-md-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-md-push-19{left:79.16666667%}.ant-col-md-pull-19{right:79.16666667%}.ant-col-md-offset-19{margin-left:79.16666667%}.ant-col-md-order-19{-ms-flex-order:19;order:19}.ant-col-md-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-md-push-18{left:75%}.ant-col-md-pull-18{right:75%}.ant-col-md-offset-18{margin-left:75%}.ant-col-md-order-18{-ms-flex-order:18;order:18}.ant-col-md-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-md-push-17{left:70.83333333%}.ant-col-md-pull-17{right:70.83333333%}.ant-col-md-offset-17{margin-left:70.83333333%}.ant-col-md-order-17{-ms-flex-order:17;order:17}.ant-col-md-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-md-push-16{left:66.66666667%}.ant-col-md-pull-16{right:66.66666667%}.ant-col-md-offset-16{margin-left:66.66666667%}.ant-col-md-order-16{-ms-flex-order:16;order:16}.ant-col-md-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-md-push-15{left:62.5%}.ant-col-md-pull-15{right:62.5%}.ant-col-md-offset-15{margin-left:62.5%}.ant-col-md-order-15{-ms-flex-order:15;order:15}.ant-col-md-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-md-push-14{left:58.33333333%}.ant-col-md-pull-14{right:58.33333333%}.ant-col-md-offset-14{margin-left:58.33333333%}.ant-col-md-order-14{-ms-flex-order:14;order:14}.ant-col-md-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-md-push-13{left:54.16666667%}.ant-col-md-pull-13{right:54.16666667%}.ant-col-md-offset-13{margin-left:54.16666667%}.ant-col-md-order-13{-ms-flex-order:13;order:13}.ant-col-md-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-md-push-12{left:50%}.ant-col-md-pull-12{right:50%}.ant-col-md-offset-12{margin-left:50%}.ant-col-md-order-12{-ms-flex-order:12;order:12}.ant-col-md-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-md-push-11{left:45.83333333%}.ant-col-md-pull-11{right:45.83333333%}.ant-col-md-offset-11{margin-left:45.83333333%}.ant-col-md-order-11{-ms-flex-order:11;order:11}.ant-col-md-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-md-push-10{left:41.66666667%}.ant-col-md-pull-10{right:41.66666667%}.ant-col-md-offset-10{margin-left:41.66666667%}.ant-col-md-order-10{-ms-flex-order:10;order:10}.ant-col-md-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-md-push-9{left:37.5%}.ant-col-md-pull-9{right:37.5%}.ant-col-md-offset-9{margin-left:37.5%}.ant-col-md-order-9{-ms-flex-order:9;order:9}.ant-col-md-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-md-push-8{left:33.33333333%}.ant-col-md-pull-8{right:33.33333333%}.ant-col-md-offset-8{margin-left:33.33333333%}.ant-col-md-order-8{-ms-flex-order:8;order:8}.ant-col-md-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-md-push-7{left:29.16666667%}.ant-col-md-pull-7{right:29.16666667%}.ant-col-md-offset-7{margin-left:29.16666667%}.ant-col-md-order-7{-ms-flex-order:7;order:7}.ant-col-md-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-md-push-6{left:25%}.ant-col-md-pull-6{right:25%}.ant-col-md-offset-6{margin-left:25%}.ant-col-md-order-6{-ms-flex-order:6;order:6}.ant-col-md-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-md-push-5{left:20.83333333%}.ant-col-md-pull-5{right:20.83333333%}.ant-col-md-offset-5{margin-left:20.83333333%}.ant-col-md-order-5{-ms-flex-order:5;order:5}.ant-col-md-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-md-push-4{left:16.66666667%}.ant-col-md-pull-4{right:16.66666667%}.ant-col-md-offset-4{margin-left:16.66666667%}.ant-col-md-order-4{-ms-flex-order:4;order:4}.ant-col-md-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-md-push-3{left:12.5%}.ant-col-md-pull-3{right:12.5%}.ant-col-md-offset-3{margin-left:12.5%}.ant-col-md-order-3{-ms-flex-order:3;order:3}.ant-col-md-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-md-push-2{left:8.33333333%}.ant-col-md-pull-2{right:8.33333333%}.ant-col-md-offset-2{margin-left:8.33333333%}.ant-col-md-order-2{-ms-flex-order:2;order:2}.ant-col-md-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-md-push-1{left:4.16666667%}.ant-col-md-pull-1{right:4.16666667%}.ant-col-md-offset-1{margin-left:4.16666667%}.ant-col-md-order-1{-ms-flex-order:1;order:1}.ant-col-md-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-md-push-0{left:auto}.ant-col-md-pull-0{right:auto}.ant-col-md-offset-0{margin-left:0}.ant-col-md-order-0{-ms-flex-order:0;order:0}}@media (min-width:992px){.ant-col-lg-1,.ant-col-lg-2,.ant-col-lg-3,.ant-col-lg-4,.ant-col-lg-5,.ant-col-lg-6,.ant-col-lg-7,.ant-col-lg-8,.ant-col-lg-9,.ant-col-lg-10,.ant-col-lg-11,.ant-col-lg-12,.ant-col-lg-13,.ant-col-lg-14,.ant-col-lg-15,.ant-col-lg-16,.ant-col-lg-17,.ant-col-lg-18,.ant-col-lg-19,.ant-col-lg-20,.ant-col-lg-21,.ant-col-lg-22,.ant-col-lg-23,.ant-col-lg-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-lg-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-lg-push-24{left:100%}.ant-col-lg-pull-24{right:100%}.ant-col-lg-offset-24{margin-left:100%}.ant-col-lg-order-24{-ms-flex-order:24;order:24}.ant-col-lg-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-lg-push-23{left:95.83333333%}.ant-col-lg-pull-23{right:95.83333333%}.ant-col-lg-offset-23{margin-left:95.83333333%}.ant-col-lg-order-23{-ms-flex-order:23;order:23}.ant-col-lg-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-lg-push-22{left:91.66666667%}.ant-col-lg-pull-22{right:91.66666667%}.ant-col-lg-offset-22{margin-left:91.66666667%}.ant-col-lg-order-22{-ms-flex-order:22;order:22}.ant-col-lg-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-lg-push-21{left:87.5%}.ant-col-lg-pull-21{right:87.5%}.ant-col-lg-offset-21{margin-left:87.5%}.ant-col-lg-order-21{-ms-flex-order:21;order:21}.ant-col-lg-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-lg-push-20{left:83.33333333%}.ant-col-lg-pull-20{right:83.33333333%}.ant-col-lg-offset-20{margin-left:83.33333333%}.ant-col-lg-order-20{-ms-flex-order:20;order:20}.ant-col-lg-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-lg-push-19{left:79.16666667%}.ant-col-lg-pull-19{right:79.16666667%}.ant-col-lg-offset-19{margin-left:79.16666667%}.ant-col-lg-order-19{-ms-flex-order:19;order:19}.ant-col-lg-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-lg-push-18{left:75%}.ant-col-lg-pull-18{right:75%}.ant-col-lg-offset-18{margin-left:75%}.ant-col-lg-order-18{-ms-flex-order:18;order:18}.ant-col-lg-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-lg-push-17{left:70.83333333%}.ant-col-lg-pull-17{right:70.83333333%}.ant-col-lg-offset-17{margin-left:70.83333333%}.ant-col-lg-order-17{-ms-flex-order:17;order:17}.ant-col-lg-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-lg-push-16{left:66.66666667%}.ant-col-lg-pull-16{right:66.66666667%}.ant-col-lg-offset-16{margin-left:66.66666667%}.ant-col-lg-order-16{-ms-flex-order:16;order:16}.ant-col-lg-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-lg-push-15{left:62.5%}.ant-col-lg-pull-15{right:62.5%}.ant-col-lg-offset-15{margin-left:62.5%}.ant-col-lg-order-15{-ms-flex-order:15;order:15}.ant-col-lg-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-lg-push-14{left:58.33333333%}.ant-col-lg-pull-14{right:58.33333333%}.ant-col-lg-offset-14{margin-left:58.33333333%}.ant-col-lg-order-14{-ms-flex-order:14;order:14}.ant-col-lg-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-lg-push-13{left:54.16666667%}.ant-col-lg-pull-13{right:54.16666667%}.ant-col-lg-offset-13{margin-left:54.16666667%}.ant-col-lg-order-13{-ms-flex-order:13;order:13}.ant-col-lg-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-lg-push-12{left:50%}.ant-col-lg-pull-12{right:50%}.ant-col-lg-offset-12{margin-left:50%}.ant-col-lg-order-12{-ms-flex-order:12;order:12}.ant-col-lg-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-lg-push-11{left:45.83333333%}.ant-col-lg-pull-11{right:45.83333333%}.ant-col-lg-offset-11{margin-left:45.83333333%}.ant-col-lg-order-11{-ms-flex-order:11;order:11}.ant-col-lg-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-lg-push-10{left:41.66666667%}.ant-col-lg-pull-10{right:41.66666667%}.ant-col-lg-offset-10{margin-left:41.66666667%}.ant-col-lg-order-10{-ms-flex-order:10;order:10}.ant-col-lg-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-lg-push-9{left:37.5%}.ant-col-lg-pull-9{right:37.5%}.ant-col-lg-offset-9{margin-left:37.5%}.ant-col-lg-order-9{-ms-flex-order:9;order:9}.ant-col-lg-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-lg-push-8{left:33.33333333%}.ant-col-lg-pull-8{right:33.33333333%}.ant-col-lg-offset-8{margin-left:33.33333333%}.ant-col-lg-order-8{-ms-flex-order:8;order:8}.ant-col-lg-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-lg-push-7{left:29.16666667%}.ant-col-lg-pull-7{right:29.16666667%}.ant-col-lg-offset-7{margin-left:29.16666667%}.ant-col-lg-order-7{-ms-flex-order:7;order:7}.ant-col-lg-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-lg-push-6{left:25%}.ant-col-lg-pull-6{right:25%}.ant-col-lg-offset-6{margin-left:25%}.ant-col-lg-order-6{-ms-flex-order:6;order:6}.ant-col-lg-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-lg-push-5{left:20.83333333%}.ant-col-lg-pull-5{right:20.83333333%}.ant-col-lg-offset-5{margin-left:20.83333333%}.ant-col-lg-order-5{-ms-flex-order:5;order:5}.ant-col-lg-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-lg-push-4{left:16.66666667%}.ant-col-lg-pull-4{right:16.66666667%}.ant-col-lg-offset-4{margin-left:16.66666667%}.ant-col-lg-order-4{-ms-flex-order:4;order:4}.ant-col-lg-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-lg-push-3{left:12.5%}.ant-col-lg-pull-3{right:12.5%}.ant-col-lg-offset-3{margin-left:12.5%}.ant-col-lg-order-3{-ms-flex-order:3;order:3}.ant-col-lg-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-lg-push-2{left:8.33333333%}.ant-col-lg-pull-2{right:8.33333333%}.ant-col-lg-offset-2{margin-left:8.33333333%}.ant-col-lg-order-2{-ms-flex-order:2;order:2}.ant-col-lg-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-lg-push-1{left:4.16666667%}.ant-col-lg-pull-1{right:4.16666667%}.ant-col-lg-offset-1{margin-left:4.16666667%}.ant-col-lg-order-1{-ms-flex-order:1;order:1}.ant-col-lg-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-lg-push-0{left:auto}.ant-col-lg-pull-0{right:auto}.ant-col-lg-offset-0{margin-left:0}.ant-col-lg-order-0{-ms-flex-order:0;order:0}}@media (min-width:1200px){.ant-col-xl-1,.ant-col-xl-2,.ant-col-xl-3,.ant-col-xl-4,.ant-col-xl-5,.ant-col-xl-6,.ant-col-xl-7,.ant-col-xl-8,.ant-col-xl-9,.ant-col-xl-10,.ant-col-xl-11,.ant-col-xl-12,.ant-col-xl-13,.ant-col-xl-14,.ant-col-xl-15,.ant-col-xl-16,.ant-col-xl-17,.ant-col-xl-18,.ant-col-xl-19,.ant-col-xl-20,.ant-col-xl-21,.ant-col-xl-22,.ant-col-xl-23,.ant-col-xl-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-xl-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-xl-push-24{left:100%}.ant-col-xl-pull-24{right:100%}.ant-col-xl-offset-24{margin-left:100%}.ant-col-xl-order-24{-ms-flex-order:24;order:24}.ant-col-xl-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-xl-push-23{left:95.83333333%}.ant-col-xl-pull-23{right:95.83333333%}.ant-col-xl-offset-23{margin-left:95.83333333%}.ant-col-xl-order-23{-ms-flex-order:23;order:23}.ant-col-xl-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-xl-push-22{left:91.66666667%}.ant-col-xl-pull-22{right:91.66666667%}.ant-col-xl-offset-22{margin-left:91.66666667%}.ant-col-xl-order-22{-ms-flex-order:22;order:22}.ant-col-xl-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-xl-push-21{left:87.5%}.ant-col-xl-pull-21{right:87.5%}.ant-col-xl-offset-21{margin-left:87.5%}.ant-col-xl-order-21{-ms-flex-order:21;order:21}.ant-col-xl-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-xl-push-20{left:83.33333333%}.ant-col-xl-pull-20{right:83.33333333%}.ant-col-xl-offset-20{margin-left:83.33333333%}.ant-col-xl-order-20{-ms-flex-order:20;order:20}.ant-col-xl-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-xl-push-19{left:79.16666667%}.ant-col-xl-pull-19{right:79.16666667%}.ant-col-xl-offset-19{margin-left:79.16666667%}.ant-col-xl-order-19{-ms-flex-order:19;order:19}.ant-col-xl-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-xl-push-18{left:75%}.ant-col-xl-pull-18{right:75%}.ant-col-xl-offset-18{margin-left:75%}.ant-col-xl-order-18{-ms-flex-order:18;order:18}.ant-col-xl-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-xl-push-17{left:70.83333333%}.ant-col-xl-pull-17{right:70.83333333%}.ant-col-xl-offset-17{margin-left:70.83333333%}.ant-col-xl-order-17{-ms-flex-order:17;order:17}.ant-col-xl-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-xl-push-16{left:66.66666667%}.ant-col-xl-pull-16{right:66.66666667%}.ant-col-xl-offset-16{margin-left:66.66666667%}.ant-col-xl-order-16{-ms-flex-order:16;order:16}.ant-col-xl-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-xl-push-15{left:62.5%}.ant-col-xl-pull-15{right:62.5%}.ant-col-xl-offset-15{margin-left:62.5%}.ant-col-xl-order-15{-ms-flex-order:15;order:15}.ant-col-xl-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-xl-push-14{left:58.33333333%}.ant-col-xl-pull-14{right:58.33333333%}.ant-col-xl-offset-14{margin-left:58.33333333%}.ant-col-xl-order-14{-ms-flex-order:14;order:14}.ant-col-xl-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-xl-push-13{left:54.16666667%}.ant-col-xl-pull-13{right:54.16666667%}.ant-col-xl-offset-13{margin-left:54.16666667%}.ant-col-xl-order-13{-ms-flex-order:13;order:13}.ant-col-xl-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-xl-push-12{left:50%}.ant-col-xl-pull-12{right:50%}.ant-col-xl-offset-12{margin-left:50%}.ant-col-xl-order-12{-ms-flex-order:12;order:12}.ant-col-xl-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-xl-push-11{left:45.83333333%}.ant-col-xl-pull-11{right:45.83333333%}.ant-col-xl-offset-11{margin-left:45.83333333%}.ant-col-xl-order-11{-ms-flex-order:11;order:11}.ant-col-xl-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-xl-push-10{left:41.66666667%}.ant-col-xl-pull-10{right:41.66666667%}.ant-col-xl-offset-10{margin-left:41.66666667%}.ant-col-xl-order-10{-ms-flex-order:10;order:10}.ant-col-xl-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-xl-push-9{left:37.5%}.ant-col-xl-pull-9{right:37.5%}.ant-col-xl-offset-9{margin-left:37.5%}.ant-col-xl-order-9{-ms-flex-order:9;order:9}.ant-col-xl-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-xl-push-8{left:33.33333333%}.ant-col-xl-pull-8{right:33.33333333%}.ant-col-xl-offset-8{margin-left:33.33333333%}.ant-col-xl-order-8{-ms-flex-order:8;order:8}.ant-col-xl-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-xl-push-7{left:29.16666667%}.ant-col-xl-pull-7{right:29.16666667%}.ant-col-xl-offset-7{margin-left:29.16666667%}.ant-col-xl-order-7{-ms-flex-order:7;order:7}.ant-col-xl-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-xl-push-6{left:25%}.ant-col-xl-pull-6{right:25%}.ant-col-xl-offset-6{margin-left:25%}.ant-col-xl-order-6{-ms-flex-order:6;order:6}.ant-col-xl-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-xl-push-5{left:20.83333333%}.ant-col-xl-pull-5{right:20.83333333%}.ant-col-xl-offset-5{margin-left:20.83333333%}.ant-col-xl-order-5{-ms-flex-order:5;order:5}.ant-col-xl-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-xl-push-4{left:16.66666667%}.ant-col-xl-pull-4{right:16.66666667%}.ant-col-xl-offset-4{margin-left:16.66666667%}.ant-col-xl-order-4{-ms-flex-order:4;order:4}.ant-col-xl-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-xl-push-3{left:12.5%}.ant-col-xl-pull-3{right:12.5%}.ant-col-xl-offset-3{margin-left:12.5%}.ant-col-xl-order-3{-ms-flex-order:3;order:3}.ant-col-xl-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-xl-push-2{left:8.33333333%}.ant-col-xl-pull-2{right:8.33333333%}.ant-col-xl-offset-2{margin-left:8.33333333%}.ant-col-xl-order-2{-ms-flex-order:2;order:2}.ant-col-xl-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-xl-push-1{left:4.16666667%}.ant-col-xl-pull-1{right:4.16666667%}.ant-col-xl-offset-1{margin-left:4.16666667%}.ant-col-xl-order-1{-ms-flex-order:1;order:1}.ant-col-xl-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xl-push-0{left:auto}.ant-col-xl-pull-0{right:auto}.ant-col-xl-offset-0{margin-left:0}.ant-col-xl-order-0{-ms-flex-order:0;order:0}}@media (min-width:1600px){.ant-col-xxl-1,.ant-col-xxl-2,.ant-col-xxl-3,.ant-col-xxl-4,.ant-col-xxl-5,.ant-col-xxl-6,.ant-col-xxl-7,.ant-col-xxl-8,.ant-col-xxl-9,.ant-col-xxl-10,.ant-col-xxl-11,.ant-col-xxl-12,.ant-col-xxl-13,.ant-col-xxl-14,.ant-col-xxl-15,.ant-col-xxl-16,.ant-col-xxl-17,.ant-col-xxl-18,.ant-col-xxl-19,.ant-col-xxl-20,.ant-col-xxl-21,.ant-col-xxl-22,.ant-col-xxl-23,.ant-col-xxl-24{-ms-flex:0 0 auto;flex:0 0 auto;float:left}.ant-col-xxl-24{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:100%}.ant-col-xxl-push-24{left:100%}.ant-col-xxl-pull-24{right:100%}.ant-col-xxl-offset-24{margin-left:100%}.ant-col-xxl-order-24{-ms-flex-order:24;order:24}.ant-col-xxl-23{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:95.83333333%}.ant-col-xxl-push-23{left:95.83333333%}.ant-col-xxl-pull-23{right:95.83333333%}.ant-col-xxl-offset-23{margin-left:95.83333333%}.ant-col-xxl-order-23{-ms-flex-order:23;order:23}.ant-col-xxl-22{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:91.66666667%}.ant-col-xxl-push-22{left:91.66666667%}.ant-col-xxl-pull-22{right:91.66666667%}.ant-col-xxl-offset-22{margin-left:91.66666667%}.ant-col-xxl-order-22{-ms-flex-order:22;order:22}.ant-col-xxl-21{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:87.5%}.ant-col-xxl-push-21{left:87.5%}.ant-col-xxl-pull-21{right:87.5%}.ant-col-xxl-offset-21{margin-left:87.5%}.ant-col-xxl-order-21{-ms-flex-order:21;order:21}.ant-col-xxl-20{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:83.33333333%}.ant-col-xxl-push-20{left:83.33333333%}.ant-col-xxl-pull-20{right:83.33333333%}.ant-col-xxl-offset-20{margin-left:83.33333333%}.ant-col-xxl-order-20{-ms-flex-order:20;order:20}.ant-col-xxl-19{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:79.16666667%}.ant-col-xxl-push-19{left:79.16666667%}.ant-col-xxl-pull-19{right:79.16666667%}.ant-col-xxl-offset-19{margin-left:79.16666667%}.ant-col-xxl-order-19{-ms-flex-order:19;order:19}.ant-col-xxl-18{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:75%}.ant-col-xxl-push-18{left:75%}.ant-col-xxl-pull-18{right:75%}.ant-col-xxl-offset-18{margin-left:75%}.ant-col-xxl-order-18{-ms-flex-order:18;order:18}.ant-col-xxl-17{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:70.83333333%}.ant-col-xxl-push-17{left:70.83333333%}.ant-col-xxl-pull-17{right:70.83333333%}.ant-col-xxl-offset-17{margin-left:70.83333333%}.ant-col-xxl-order-17{-ms-flex-order:17;order:17}.ant-col-xxl-16{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:66.66666667%}.ant-col-xxl-push-16{left:66.66666667%}.ant-col-xxl-pull-16{right:66.66666667%}.ant-col-xxl-offset-16{margin-left:66.66666667%}.ant-col-xxl-order-16{-ms-flex-order:16;order:16}.ant-col-xxl-15{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:62.5%}.ant-col-xxl-push-15{left:62.5%}.ant-col-xxl-pull-15{right:62.5%}.ant-col-xxl-offset-15{margin-left:62.5%}.ant-col-xxl-order-15{-ms-flex-order:15;order:15}.ant-col-xxl-14{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:58.33333333%}.ant-col-xxl-push-14{left:58.33333333%}.ant-col-xxl-pull-14{right:58.33333333%}.ant-col-xxl-offset-14{margin-left:58.33333333%}.ant-col-xxl-order-14{-ms-flex-order:14;order:14}.ant-col-xxl-13{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:54.16666667%}.ant-col-xxl-push-13{left:54.16666667%}.ant-col-xxl-pull-13{right:54.16666667%}.ant-col-xxl-offset-13{margin-left:54.16666667%}.ant-col-xxl-order-13{-ms-flex-order:13;order:13}.ant-col-xxl-12{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:50%}.ant-col-xxl-push-12{left:50%}.ant-col-xxl-pull-12{right:50%}.ant-col-xxl-offset-12{margin-left:50%}.ant-col-xxl-order-12{-ms-flex-order:12;order:12}.ant-col-xxl-11{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:45.83333333%}.ant-col-xxl-push-11{left:45.83333333%}.ant-col-xxl-pull-11{right:45.83333333%}.ant-col-xxl-offset-11{margin-left:45.83333333%}.ant-col-xxl-order-11{-ms-flex-order:11;order:11}.ant-col-xxl-10{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:41.66666667%}.ant-col-xxl-push-10{left:41.66666667%}.ant-col-xxl-pull-10{right:41.66666667%}.ant-col-xxl-offset-10{margin-left:41.66666667%}.ant-col-xxl-order-10{-ms-flex-order:10;order:10}.ant-col-xxl-9{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:37.5%}.ant-col-xxl-push-9{left:37.5%}.ant-col-xxl-pull-9{right:37.5%}.ant-col-xxl-offset-9{margin-left:37.5%}.ant-col-xxl-order-9{-ms-flex-order:9;order:9}.ant-col-xxl-8{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:33.33333333%}.ant-col-xxl-push-8{left:33.33333333%}.ant-col-xxl-pull-8{right:33.33333333%}.ant-col-xxl-offset-8{margin-left:33.33333333%}.ant-col-xxl-order-8{-ms-flex-order:8;order:8}.ant-col-xxl-7{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:29.16666667%}.ant-col-xxl-push-7{left:29.16666667%}.ant-col-xxl-pull-7{right:29.16666667%}.ant-col-xxl-offset-7{margin-left:29.16666667%}.ant-col-xxl-order-7{-ms-flex-order:7;order:7}.ant-col-xxl-6{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:25%}.ant-col-xxl-push-6{left:25%}.ant-col-xxl-pull-6{right:25%}.ant-col-xxl-offset-6{margin-left:25%}.ant-col-xxl-order-6{-ms-flex-order:6;order:6}.ant-col-xxl-5{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:20.83333333%}.ant-col-xxl-push-5{left:20.83333333%}.ant-col-xxl-pull-5{right:20.83333333%}.ant-col-xxl-offset-5{margin-left:20.83333333%}.ant-col-xxl-order-5{-ms-flex-order:5;order:5}.ant-col-xxl-4{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:16.66666667%}.ant-col-xxl-push-4{left:16.66666667%}.ant-col-xxl-pull-4{right:16.66666667%}.ant-col-xxl-offset-4{margin-left:16.66666667%}.ant-col-xxl-order-4{-ms-flex-order:4;order:4}.ant-col-xxl-3{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:12.5%}.ant-col-xxl-push-3{left:12.5%}.ant-col-xxl-pull-3{right:12.5%}.ant-col-xxl-offset-3{margin-left:12.5%}.ant-col-xxl-order-3{-ms-flex-order:3;order:3}.ant-col-xxl-2{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:8.33333333%}.ant-col-xxl-push-2{left:8.33333333%}.ant-col-xxl-pull-2{right:8.33333333%}.ant-col-xxl-offset-2{margin-left:8.33333333%}.ant-col-xxl-order-2{-ms-flex-order:2;order:2}.ant-col-xxl-1{display:block;-webkit-box-sizing:border-box;box-sizing:border-box;width:4.16666667%}.ant-col-xxl-push-1{left:4.16666667%}.ant-col-xxl-pull-1{right:4.16666667%}.ant-col-xxl-offset-1{margin-left:4.16666667%}.ant-col-xxl-order-1{-ms-flex-order:1;order:1}.ant-col-xxl-0{display:none}.ant-col-push-0{left:auto}.ant-col-pull-0{right:auto}.ant-col-xxl-push-0{left:auto}.ant-col-xxl-pull-0{right:auto}.ant-col-xxl-offset-0{margin-left:0}.ant-col-xxl-order-0{-ms-flex-order:0;order:0}}.ant-carousel{margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-carousel,.ant-carousel .slick-slider{-webkit-box-sizing:border-box;box-sizing:border-box}.ant-carousel .slick-slider{position:relative;display:block;-webkit-touch-callout:none;-ms-touch-action:pan-y;touch-action:pan-y;-webkit-tap-highlight-color:transparent}.ant-carousel .slick-list{position:relative;display:block;margin:0;padding:0;overflow:hidden}.ant-carousel .slick-list:focus{outline:none}.ant-carousel .slick-list.dragging{cursor:pointer}.ant-carousel .slick-list .slick-slide{pointer-events:none}.ant-carousel .slick-list .slick-slide input.ant-checkbox-input,.ant-carousel .slick-list .slick-slide input.ant-radio-input{visibility:hidden}.ant-carousel .slick-list .slick-slide.slick-active{pointer-events:auto}.ant-carousel .slick-list .slick-slide.slick-active input.ant-checkbox-input,.ant-carousel .slick-list .slick-slide.slick-active input.ant-radio-input{visibility:visible}.ant-carousel .slick-slider .slick-list,.ant-carousel .slick-slider .slick-track{-webkit-transform:translateZ(0);transform:translateZ(0)}.ant-carousel .slick-track{position:relative;top:0;left:0;display:block}.ant-carousel .slick-track:after,.ant-carousel .slick-track:before{display:table;content:""}.ant-carousel .slick-track:after{clear:both}.slick-loading .ant-carousel .slick-track{visibility:hidden}.ant-carousel .slick-slide{display:none;float:left;height:100%;min-height:1px}[dir=rtl] .ant-carousel .slick-slide{float:right}.ant-carousel .slick-slide img{display:block}.ant-carousel .slick-slide.slick-loading img{display:none}.ant-carousel .slick-slide.dragging img{pointer-events:none}.ant-carousel .slick-initialized .slick-slide{display:block}.ant-carousel .slick-loading .slick-slide{visibility:hidden}.ant-carousel .slick-vertical .slick-slide{display:block;height:auto;border:1px solid transparent}.ant-carousel .slick-arrow.slick-hidden{display:none}.ant-carousel .slick-next,.ant-carousel .slick-prev{position:absolute;top:50%;display:block;width:20px;height:20px;margin-top:-10px;padding:0;font-size:0;line-height:0;border:0;cursor:pointer}.ant-carousel .slick-next,.ant-carousel .slick-next:focus,.ant-carousel .slick-next:hover,.ant-carousel .slick-prev,.ant-carousel .slick-prev:focus,.ant-carousel .slick-prev:hover{color:transparent;background:transparent;outline:none}.ant-carousel .slick-next:focus:before,.ant-carousel .slick-next:hover:before,.ant-carousel .slick-prev:focus:before,.ant-carousel .slick-prev:hover:before{opacity:1}.ant-carousel .slick-next.slick-disabled:before,.ant-carousel .slick-prev.slick-disabled:before{opacity:.25}.ant-carousel .slick-prev{left:-25px}.ant-carousel .slick-prev:before{content:"←"}.ant-carousel .slick-next{right:-25px}.ant-carousel .slick-next:before{content:"→"}.ant-carousel .slick-dots{position:absolute;display:block;width:100%;height:3px;margin:0;padding:0;text-align:center;list-style:none}.ant-carousel .slick-dots-bottom{bottom:12px}.ant-carousel .slick-dots-top{top:12px}.ant-carousel .slick-dots li{position:relative;display:inline-block;margin:0 2px;padding:0;text-align:center;vertical-align:top}.ant-carousel .slick-dots li button{display:block;width:16px;height:3px;padding:0;color:transparent;font-size:0;background:#fff;border:0;border-radius:1px;outline:none;cursor:pointer;opacity:.3;-webkit-transition:all .5s;transition:all .5s}.ant-carousel .slick-dots li button:focus,.ant-carousel .slick-dots li button:hover{opacity:.75}.ant-carousel .slick-dots li.slick-active button{width:24px;background:#fff;opacity:1}.ant-carousel .slick-dots li.slick-active button:focus,.ant-carousel .slick-dots li.slick-active button:hover{opacity:1}.ant-carousel-vertical .slick-dots{top:50%;bottom:auto;width:3px;height:auto;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-carousel-vertical .slick-dots-left{left:12px}.ant-carousel-vertical .slick-dots-right{right:12px}.ant-carousel-vertical .slick-dots li{margin:0 2px;vertical-align:baseline}.ant-carousel-vertical .slick-dots li button{width:3px;height:16px}.ant-carousel-vertical .slick-dots li.slick-active button{width:3px;height:24px}.ant-cascader{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-cascader-input.ant-input{position:static;width:100%;padding-right:24px;background-color:transparent!important;cursor:pointer}.ant-cascader-picker-show-search .ant-cascader-input.ant-input{position:relative}.ant-cascader-picker{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;background-color:#fff;border-radius:4px;outline:0;cursor:pointer;-webkit-transition:color .3s;transition:color .3s}.ant-cascader-picker-with-value .ant-cascader-picker-label{color:transparent}.ant-cascader-picker-disabled{color:rgba(0,0,0,.25);background:#f5f5f5;cursor:not-allowed}.ant-cascader-picker-disabled .ant-cascader-input{cursor:not-allowed}.ant-cascader-picker:focus .ant-cascader-input{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-cascader-picker-show-search.ant-cascader-picker-focused{color:rgba(0,0,0,.25)}.ant-cascader-picker-label{position:absolute;top:50%;left:0;width:100%;height:20px;margin-top:-10px;padding:0 20px 0 12px;overflow:hidden;line-height:20px;white-space:nowrap;text-overflow:ellipsis}.ant-cascader-picker-clear{position:absolute;top:50%;right:12px;z-index:2;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:12px;background:#fff;cursor:pointer;opacity:0;-webkit-transition:color .3s ease,opacity .15s ease;transition:color .3s ease,opacity .15s ease}.ant-cascader-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-cascader-picker:hover .ant-cascader-picker-clear{opacity:1}.ant-cascader-picker-arrow{position:absolute;top:50%;right:12px;z-index:1;width:12px;height:12px;margin-top:-6px;color:rgba(0,0,0,.25);font-size:12px;line-height:12px;-webkit-transition:-webkit-transform .2s;transition:-webkit-transform .2s;transition:transform .2s;transition:transform .2s,-webkit-transform .2s}.ant-cascader-picker-arrow.ant-cascader-picker-arrow-expand{-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.ant-cascader-picker-label:hover+.ant-cascader-input{border-color:#40a9ff;border-right-width:1px!important}.ant-cascader-picker-small .ant-cascader-picker-arrow,.ant-cascader-picker-small .ant-cascader-picker-clear{right:8px}.ant-cascader-menus{position:absolute;z-index:1050;font-size:14px;white-space:nowrap;background:#fff;border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-cascader-menus ol,.ant-cascader-menus ul{margin:0;list-style:none}.ant-cascader-menus-empty,.ant-cascader-menus-hidden{display:none}.ant-cascader-menus.slide-up-appear.slide-up-appear-active.ant-cascader-menus-placement-bottomLeft,.ant-cascader-menus.slide-up-enter.slide-up-enter-active.ant-cascader-menus-placement-bottomLeft{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn}.ant-cascader-menus.slide-up-appear.slide-up-appear-active.ant-cascader-menus-placement-topLeft,.ant-cascader-menus.slide-up-enter.slide-up-enter-active.ant-cascader-menus-placement-topLeft{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn}.ant-cascader-menus.slide-up-leave.slide-up-leave-active.ant-cascader-menus-placement-bottomLeft{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut}.ant-cascader-menus.slide-up-leave.slide-up-leave-active.ant-cascader-menus-placement-topLeft{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut}.ant-cascader-menu{display:inline-block;min-width:111px;height:180px;margin:0;padding:4px 0;overflow:auto;vertical-align:top;list-style:none;border-right:1px solid #e8e8e8;-ms-overflow-style:-ms-autohiding-scrollbar}.ant-cascader-menu:first-child{border-radius:4px 0 0 4px}.ant-cascader-menu:last-child{margin-right:-1px;border-right-color:transparent;border-radius:0 4px 4px 0}.ant-cascader-menu:only-child{border-radius:4px}.ant-cascader-menu-item{padding:5px 12px;line-height:22px;white-space:nowrap;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-cascader-menu-item:hover{background:#e6f7ff}.ant-cascader-menu-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-cascader-menu-item-disabled:hover{background:transparent}.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled),.ant-cascader-menu-item-active:not(.ant-cascader-menu-item-disabled):hover{font-weight:600;background-color:#fafafa}.ant-cascader-menu-item-expand{position:relative;padding-right:24px}.ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,.ant-cascader-menu-item-loading-icon{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);position:absolute;right:12px;color:rgba(0,0,0,.45)}:root .ant-cascader-menu-item-expand .ant-cascader-menu-item-expand-icon,:root .ant-cascader-menu-item-loading-icon{font-size:12px}.ant-cascader-menu-item .ant-cascader-menu-item-keyword{color:#f5222d}.ant-checkbox{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-checkbox-input:focus+.ant-checkbox-inner,.ant-checkbox-wrapper:hover .ant-checkbox-inner,.ant-checkbox:hover .ant-checkbox-inner{border-color:#1890ff}.ant-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}.ant-checkbox-wrapper:hover .ant-checkbox:after,.ant-checkbox:hover:after{visibility:visible}.ant-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;-webkit-transition:all .3s;transition:all .3s}.ant-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(0) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(0) translate(-50%,-50%);transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;-webkit-transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-checkbox-checked .ant-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(1) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(1) translate(-50%,-50%);transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;-webkit-transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-checkbox-checked .ant-checkbox-inner{background-color:#1890ff;border-color:#1890ff}.ant-checkbox-disabled{cursor:not-allowed}.ant-checkbox-disabled.ant-checkbox-checked .ant-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}.ant-checkbox-disabled .ant-checkbox-input{cursor:not-allowed}.ant-checkbox-disabled .ant-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-checkbox-disabled .ant-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;-webkit-animation-name:none;animation-name:none}.ant-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-checkbox-disabled:hover:after,.ant-checkbox-wrapper:hover .ant-checkbox-disabled:after{visibility:hidden}.ant-checkbox-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-checkbox-wrapper.ant-checkbox-wrapper-disabled{cursor:not-allowed}.ant-checkbox-wrapper+.ant-checkbox-wrapper{margin-left:8px}.ant-checkbox+span{padding-right:8px;padding-left:8px}.ant-checkbox-group{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block}.ant-checkbox-group-item{display:inline-block;margin-right:8px}.ant-checkbox-group-item:last-child{margin-right:0}.ant-checkbox-group-item+.ant-checkbox-group-item{margin-left:0}.ant-checkbox-indeterminate .ant-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-checkbox-indeterminate .ant-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;-webkit-transform:translate(-50%,-50%) scale(1);-ms-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-checkbox-indeterminate.ant-checkbox-disabled .ant-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-collapse{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:4px}.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 4px 4px}.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 16px 12px 40px;color:rgba(0,0,0,.85);line-height:22px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;top:50%;left:16px;display:inline-block;font-size:12px;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow>*{line-height:1}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{display:inline-block}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow:before{display:none}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow .ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow-icon{display:block}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{-webkit-transition:-webkit-transform .24s;transition:-webkit-transform .24s;transition:transform .24s;transition:transform .24s,-webkit-transform .24s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-extra{float:right}.ant-collapse>.ant-collapse-item>.ant-collapse-header:focus{outline:none}.ant-collapse>.ant-collapse-item.ant-collapse-no-arrow>.ant-collapse-header{padding-left:12px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{right:16px;left:auto}.ant-collapse-anim-active{-webkit-transition:height .2s cubic-bezier(.215,.61,.355,1);transition:height .2s cubic-bezier(.215,.61,.355,1)}.ant-collapse-content{overflow:hidden;color:rgba(0,0,0,.65);background-color:#fff;border-top:1px solid #d9d9d9}.ant-collapse-content>.ant-collapse-content-box{padding:16px}.ant-collapse-content-inactive{display:none}.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 4px 4px}.ant-collapse-borderless{background-color:#fafafa;border:0}.ant-collapse-borderless>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse-borderless>.ant-collapse-item:last-child,.ant-collapse-borderless>.ant-collapse-item:last-child .ant-collapse-header{border-radius:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content{background-color:transparent;border-top:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box{padding-top:4px}.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header,.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header>.arrow{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-comment{position:relative}.ant-comment-inner{display:-ms-flexbox;display:flex;padding:16px 0}.ant-comment-avatar{position:relative;-ms-flex-negative:0;flex-shrink:0;margin-right:12px;cursor:pointer}.ant-comment-avatar img{width:32px;height:32px;border-radius:50%}.ant-comment-content{position:relative;-ms-flex:1 1 auto;flex:1 1 auto;min-width:1px;font-size:14px;word-wrap:break-word}.ant-comment-content-author{display:-ms-flexbox;display:flex;-ms-flex-wrap:wrap;flex-wrap:wrap;-ms-flex-pack:start;justify-content:flex-start;margin-bottom:4px;font-size:14px}.ant-comment-content-author>a,.ant-comment-content-author>span{padding-right:8px;font-size:12px;line-height:18px}.ant-comment-content-author-name{color:rgba(0,0,0,.45);font-size:14px;-webkit-transition:color .3s;transition:color .3s}.ant-comment-content-author-name>*,.ant-comment-content-author-name>:hover{color:rgba(0,0,0,.45)}.ant-comment-content-author-time{color:#ccc;white-space:nowrap;cursor:auto}.ant-comment-content-detail p{white-space:pre-wrap}.ant-comment-actions{margin-top:12px;padding-left:0}.ant-comment-actions>li{display:inline-block;color:rgba(0,0,0,.45)}.ant-comment-actions>li>span{padding-right:10px;color:rgba(0,0,0,.45);font-size:12px;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-comment-actions>li>span:hover{color:#595959}.ant-comment-nested{margin-left:44px}.ant-calendar-picker-container{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;z-index:1050;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-topRight,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-topRight{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn}.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-appear.slide-up-appear-active.ant-calendar-picker-container-placement-bottomRight,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-enter.slide-up-enter-active.ant-calendar-picker-container-placement-bottomRight{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn}.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-topLeft,.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-topRight{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut}.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-bottomLeft,.ant-calendar-picker-container.slide-up-leave.slide-up-leave-active.ant-calendar-picker-container-placement-bottomRight{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut}.ant-calendar-picker{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;outline:none;cursor:text;-webkit-transition:opacity .3s;transition:opacity .3s}.ant-calendar-picker-input{outline:none}.ant-calendar-picker-input.ant-input{line-height:1.5}.ant-calendar-picker-input.ant-input-sm{padding-top:0;padding-bottom:0}.ant-calendar-picker:hover .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff}.ant-calendar-picker:focus .ant-calendar-picker-input:not(.ant-input-disabled){border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-calendar-picker-clear,.ant-calendar-picker-icon{position:absolute;top:50%;right:12px;z-index:1;width:14px;height:14px;margin-top:-7px;font-size:12px;line-height:14px;-webkit-transition:all .3s;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-calendar-picker-clear{z-index:2;color:rgba(0,0,0,.25);font-size:14px;background:#fff;cursor:pointer;opacity:0;pointer-events:none}.ant-calendar-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-calendar-picker:hover .ant-calendar-picker-clear{opacity:1;pointer-events:auto}.ant-calendar-picker-icon{display:inline-block;color:rgba(0,0,0,.25);font-size:14px;line-height:1}.ant-input-disabled+.ant-calendar-picker-icon{cursor:not-allowed}.ant-calendar-picker-small .ant-calendar-picker-clear,.ant-calendar-picker-small .ant-calendar-picker-icon{right:8px}.ant-calendar{position:relative;width:280px;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid #fff;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-calendar-input-wrap{height:34px;padding:6px 10px;border-bottom:1px solid #e8e8e8}.ant-calendar-input{width:100%;height:22px;color:rgba(0,0,0,.65);background:#fff;border:0;outline:0;cursor:auto}.ant-calendar-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-calendar-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-week-number{width:286px}.ant-calendar-week-number-cell{text-align:center}.ant-calendar-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-calendar-header a:hover{color:#40a9ff}.ant-calendar-header .ant-calendar-century-select,.ant-calendar-header .ant-calendar-decade-select,.ant-calendar-header .ant-calendar-month-select,.ant-calendar-header .ant-calendar-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-header .ant-calendar-century-select-arrow,.ant-calendar-header .ant-calendar-decade-select-arrow,.ant-calendar-header .ant-calendar-month-select-arrow,.ant-calendar-header .ant-calendar-year-select-arrow{display:none}.ant-calendar-header .ant-calendar-next-century-btn,.ant-calendar-header .ant-calendar-next-decade-btn,.ant-calendar-header .ant-calendar-next-month-btn,.ant-calendar-header .ant-calendar-next-year-btn,.ant-calendar-header .ant-calendar-prev-century-btn,.ant-calendar-header .ant-calendar-prev-decade-btn,.ant-calendar-header .ant-calendar-prev-month-btn,.ant-calendar-header .ant-calendar-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,"Hiragino Sans GB","Microsoft Yahei","Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-header .ant-calendar-prev-century-btn,.ant-calendar-header .ant-calendar-prev-decade-btn,.ant-calendar-header .ant-calendar-prev-year-btn{left:7px;height:100%}.ant-calendar-header .ant-calendar-prev-century-btn:after,.ant-calendar-header .ant-calendar-prev-century-btn:before,.ant-calendar-header .ant-calendar-prev-decade-btn:after,.ant-calendar-header .ant-calendar-prev-decade-btn:before,.ant-calendar-header .ant-calendar-prev-year-btn:after,.ant-calendar-header .ant-calendar-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-header .ant-calendar-prev-century-btn:hover:after,.ant-calendar-header .ant-calendar-prev-century-btn:hover:before,.ant-calendar-header .ant-calendar-prev-decade-btn:hover:after,.ant-calendar-header .ant-calendar-prev-decade-btn:hover:before,.ant-calendar-header .ant-calendar-prev-year-btn:hover:after,.ant-calendar-header .ant-calendar-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-prev-century-btn:after,.ant-calendar-header .ant-calendar-prev-decade-btn:after,.ant-calendar-header .ant-calendar-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-header .ant-calendar-next-century-btn,.ant-calendar-header .ant-calendar-next-decade-btn,.ant-calendar-header .ant-calendar-next-year-btn{right:7px;height:100%}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-header .ant-calendar-next-century-btn:hover:after,.ant-calendar-header .ant-calendar-next-century-btn:hover:before,.ant-calendar-header .ant-calendar-next-decade-btn:hover:after,.ant-calendar-header .ant-calendar-next-decade-btn:hover:before,.ant-calendar-header .ant-calendar-next-year-btn:hover:after,.ant-calendar-header .ant-calendar-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:after{display:none}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-header .ant-calendar-next-century-btn:before,.ant-calendar-header .ant-calendar-next-decade-btn:before,.ant-calendar-header .ant-calendar-next-year-btn:before{position:relative;left:3px}.ant-calendar-header .ant-calendar-next-century-btn:after,.ant-calendar-header .ant-calendar-next-decade-btn:after,.ant-calendar-header .ant-calendar-next-year-btn:after{display:inline-block}.ant-calendar-header .ant-calendar-prev-month-btn{left:29px;height:100%}.ant-calendar-header .ant-calendar-prev-month-btn:after,.ant-calendar-header .ant-calendar-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-header .ant-calendar-prev-month-btn:hover:after,.ant-calendar-header .ant-calendar-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-prev-month-btn:after{display:none}.ant-calendar-header .ant-calendar-next-month-btn{right:29px;height:100%}.ant-calendar-header .ant-calendar-next-month-btn:after,.ant-calendar-header .ant-calendar-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-header .ant-calendar-next-month-btn:hover:after,.ant-calendar-header .ant-calendar-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-header .ant-calendar-next-month-btn:after{display:none}.ant-calendar-header .ant-calendar-next-month-btn:after,.ant-calendar-header .ant-calendar-next-month-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-body{padding:8px 12px}.ant-calendar table{width:100%;max-width:100%;background-color:transparent;border-collapse:collapse}.ant-calendar table,.ant-calendar td,.ant-calendar th{text-align:center;border:0}.ant-calendar-calendar-table{margin-bottom:0;border-spacing:0}.ant-calendar-column-header{width:33px;padding:6px 0;line-height:18px;text-align:center}.ant-calendar-column-header .ant-calendar-column-header-inner{display:block;font-weight:400}.ant-calendar-week-number-header .ant-calendar-column-header-inner{display:none}.ant-calendar-cell{height:30px;padding:3px 0}.ant-calendar-date{display:block;width:24px;height:24px;margin:0 auto;padding:0;color:rgba(0,0,0,.65);line-height:22px;text-align:center;background:transparent;border:1px solid transparent;border-radius:2px;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-calendar-date-panel{position:relative;outline:none}.ant-calendar-date:hover{background:#e6f7ff;cursor:pointer}.ant-calendar-date:active{color:#fff;background:#40a9ff}.ant-calendar-today .ant-calendar-date{color:#1890ff;font-weight:700;border-color:#1890ff}.ant-calendar-selected-day .ant-calendar-date{background:#bae7ff}.ant-calendar-last-month-cell .ant-calendar-date,.ant-calendar-last-month-cell .ant-calendar-date:hover,.ant-calendar-next-month-btn-day .ant-calendar-date,.ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgba(0,0,0,.25);background:transparent;border-color:transparent}.ant-calendar-disabled-cell .ant-calendar-date{position:relative;width:auto;color:rgba(0,0,0,.25);background:#f5f5f5;border:1px solid transparent;border-radius:0;cursor:not-allowed}.ant-calendar-disabled-cell .ant-calendar-date:hover{background:#f5f5f5}.ant-calendar-disabled-cell.ant-calendar-selected-day .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;background:rgba(0,0,0,.1);border-radius:2px;content:""}.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date{position:relative;padding-right:5px;padding-left:5px}.ant-calendar-disabled-cell.ant-calendar-today .ant-calendar-date:before{position:absolute;top:-1px;left:5px;width:24px;height:24px;border:1px solid rgba(0,0,0,.25);border-radius:2px;content:" "}.ant-calendar-disabled-cell-first-of-row .ant-calendar-date{border-top-left-radius:4px;border-bottom-left-radius:4px}.ant-calendar-disabled-cell-last-of-row .ant-calendar-date{border-top-right-radius:4px;border-bottom-right-radius:4px}.ant-calendar-footer{padding:0 12px;line-height:38px;border-top:1px solid #e8e8e8}.ant-calendar-footer:empty{border-top:0}.ant-calendar-footer-btn{display:block;text-align:center}.ant-calendar-footer-extra{text-align:left}.ant-calendar .ant-calendar-clear-btn,.ant-calendar .ant-calendar-today-btn{display:inline-block;margin:0 0 0 8px;text-align:center}.ant-calendar .ant-calendar-clear-btn-disabled,.ant-calendar .ant-calendar-today-btn-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-calendar .ant-calendar-clear-btn:only-child,.ant-calendar .ant-calendar-today-btn:only-child{margin:0}.ant-calendar .ant-calendar-clear-btn{position:absolute;top:7px;right:5px;display:none;width:20px;height:20px;margin:0;overflow:hidden;line-height:20px;text-align:center;text-indent:-76px}.ant-calendar .ant-calendar-clear-btn:after{display:inline-block;width:20px;color:rgba(0,0,0,.25);font-size:14px;line-height:1;text-indent:43px;-webkit-transition:color .3s ease;transition:color .3s ease}.ant-calendar .ant-calendar-clear-btn:hover:after{color:rgba(0,0,0,.45)}.ant-calendar .ant-calendar-ok-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;-webkit-box-shadow:0 2px 0 rgba(0,0,0,.015);box-shadow:0 2px 0 rgba(0,0,0,.015);cursor:pointer;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-ms-touch-action:manipulation;touch-action:manipulation;height:32px;color:#fff;background-color:#1890ff;border:1px solid #1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);-webkit-box-shadow:0 2px 0 rgba(0,0,0,.045);box-shadow:0 2px 0 rgba(0,0,0,.045);height:24px;padding:0 7px;font-size:14px;border-radius:4px;line-height:22px}.ant-calendar .ant-calendar-ok-btn>.anticon{line-height:1}.ant-calendar .ant-calendar-ok-btn,.ant-calendar .ant-calendar-ok-btn:active,.ant-calendar .ant-calendar-ok-btn:focus{outline:0}.ant-calendar .ant-calendar-ok-btn:not([disabled]):hover{text-decoration:none}.ant-calendar .ant-calendar-ok-btn:not([disabled]):active{outline:0;-webkit-box-shadow:none;box-shadow:none}.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn[disabled]{cursor:not-allowed}.ant-calendar .ant-calendar-ok-btn.disabled>*,.ant-calendar .ant-calendar-ok-btn[disabled]>*{pointer-events:none}.ant-calendar .ant-calendar-ok-btn-lg{height:40px;padding:0 15px;font-size:16px;border-radius:4px}.ant-calendar .ant-calendar-ok-btn-sm{height:24px;padding:0 7px;font-size:14px;border-radius:4px}.ant-calendar .ant-calendar-ok-btn>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn:focus,.ant-calendar .ant-calendar-ok-btn:hover{color:#fff;background-color:#40a9ff;border-color:#40a9ff}.ant-calendar .ant-calendar-ok-btn:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:hover>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn.active,.ant-calendar .ant-calendar-ok-btn:active{color:#fff;background-color:#096dd9;border-color:#096dd9}.ant-calendar .ant-calendar-ok-btn.active>a:only-child,.ant-calendar .ant-calendar-ok-btn:active>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn:active>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar .ant-calendar-ok-btn-disabled,.ant-calendar .ant-calendar-ok-btn-disabled.active,.ant-calendar .ant-calendar-ok-btn-disabled:active,.ant-calendar .ant-calendar-ok-btn-disabled:focus,.ant-calendar .ant-calendar-ok-btn-disabled:hover,.ant-calendar .ant-calendar-ok-btn.disabled,.ant-calendar .ant-calendar-ok-btn.disabled.active,.ant-calendar .ant-calendar-ok-btn.disabled:active,.ant-calendar .ant-calendar-ok-btn.disabled:focus,.ant-calendar .ant-calendar-ok-btn.disabled:hover,.ant-calendar .ant-calendar-ok-btn[disabled],.ant-calendar .ant-calendar-ok-btn[disabled].active,.ant-calendar .ant-calendar-ok-btn[disabled]:active,.ant-calendar .ant-calendar-ok-btn[disabled]:focus,.ant-calendar .ant-calendar-ok-btn[disabled]:hover{color:rgba(0,0,0,.25);background-color:#f5f5f5;border-color:#d9d9d9;text-shadow:none;-webkit-box-shadow:none;box-shadow:none}.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child{color:currentColor}.ant-calendar .ant-calendar-ok-btn-disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn-disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled.active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn.disabled>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled].active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:active>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:focus>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]:hover>a:only-child:after,.ant-calendar .ant-calendar-ok-btn[disabled]>a:only-child:after{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;content:""}.ant-calendar-range-picker-input{width:44%;height:99%;text-align:center;background-color:transparent;border:0;outline:0}.ant-calendar-range-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-range-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-range-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-range-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-range-picker-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-calendar-range-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-range-picker-input[disabled]{cursor:not-allowed}.ant-calendar-range-picker-separator{display:inline-block;min-width:10px;height:100%;color:rgba(0,0,0,.45);white-space:nowrap;text-align:center;vertical-align:top;pointer-events:none}.ant-calendar-range{width:552px;overflow:hidden}.ant-calendar-range .ant-calendar-date-panel:after{display:block;clear:both;height:0;visibility:hidden;content:"."}.ant-calendar-range-part{position:relative;width:50%}.ant-calendar-range-left{float:left}.ant-calendar-range-left .ant-calendar-time-picker-inner{border-right:1px solid #e8e8e8}.ant-calendar-range-right{float:right}.ant-calendar-range-right .ant-calendar-time-picker-inner{border-left:1px solid #e8e8e8}.ant-calendar-range-middle{position:absolute;left:50%;z-index:1;height:34px;margin:1px 0 0;padding:0 200px 0 0;color:rgba(0,0,0,.45);line-height:34px;text-align:center;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%);pointer-events:none}.ant-calendar-range-right .ant-calendar-date-input-wrap{margin-left:-90px}.ant-calendar-range.ant-calendar-time .ant-calendar-range-middle{padding:0 10px 0 0;-webkit-transform:translateX(-50%);-ms-transform:translateX(-50%);transform:translateX(-50%)}.ant-calendar-range .ant-calendar-today :not(.ant-calendar-disabled-cell) :not(.ant-calendar-last-month-cell) :not(.ant-calendar-next-month-btn-day) .ant-calendar-date{color:#1890ff;background:#bae7ff;border-color:#1890ff}.ant-calendar-range .ant-calendar-selected-end-date .ant-calendar-date,.ant-calendar-range .ant-calendar-selected-start-date .ant-calendar-date{color:#fff;background:#1890ff;border:1px solid transparent}.ant-calendar-range .ant-calendar-selected-end-date .ant-calendar-date:hover,.ant-calendar-range .ant-calendar-selected-start-date .ant-calendar-date:hover{background:#1890ff}.ant-calendar-range.ant-calendar-time .ant-calendar-range-right .ant-calendar-date-input-wrap{margin-left:0}.ant-calendar-range .ant-calendar-input-wrap{position:relative;height:34px}.ant-calendar-range .ant-calendar-input,.ant-calendar-range .ant-calendar-time-picker-input{position:relative;display:inline-block;width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border-radius:4px;-webkit-transition:all .3s;transition:all .3s;height:24px;padding:4px 0;line-height:24px;border:0;-webkit-box-shadow:none;box-shadow:none}.ant-calendar-range .ant-calendar-input::-moz-placeholder,.ant-calendar-range .ant-calendar-time-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-calendar-range .ant-calendar-input:-ms-input-placeholder,.ant-calendar-range .ant-calendar-time-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-calendar-range .ant-calendar-input::-webkit-input-placeholder,.ant-calendar-range .ant-calendar-time-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-calendar-range .ant-calendar-input:-moz-placeholder-shown,.ant-calendar-range .ant-calendar-time-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-calendar-range .ant-calendar-input:-ms-input-placeholder,.ant-calendar-range .ant-calendar-time-picker-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-calendar-range .ant-calendar-input:placeholder-shown,.ant-calendar-range .ant-calendar-time-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-calendar-range .ant-calendar-input:hover,.ant-calendar-range .ant-calendar-time-picker-input:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-calendar-range .ant-calendar-input:focus,.ant-calendar-range .ant-calendar-time-picker-input:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-calendar-range .ant-calendar-input-disabled,.ant-calendar-range .ant-calendar-time-picker-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-calendar-range .ant-calendar-input-disabled:hover,.ant-calendar-range .ant-calendar-time-picker-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-calendar-range .ant-calendar-input[disabled],.ant-calendar-range .ant-calendar-time-picker-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-calendar-range .ant-calendar-input[disabled]:hover,.ant-calendar-range .ant-calendar-time-picker-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-calendar-range .ant-calendar-input,textarea.ant-calendar-range .ant-calendar-time-picker-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-calendar-range .ant-calendar-input-lg,.ant-calendar-range .ant-calendar-time-picker-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-calendar-range .ant-calendar-input-sm,.ant-calendar-range .ant-calendar-time-picker-input-sm{height:24px;padding:1px 7px}.ant-calendar-range .ant-calendar-input:focus,.ant-calendar-range .ant-calendar-time-picker-input:focus{-webkit-box-shadow:none;box-shadow:none}.ant-calendar-range .ant-calendar-time-picker-icon{display:none}.ant-calendar-range.ant-calendar-week-number{width:574px}.ant-calendar-range.ant-calendar-week-number .ant-calendar-range-part{width:286px}.ant-calendar-range .ant-calendar-decade-panel,.ant-calendar-range .ant-calendar-month-panel,.ant-calendar-range .ant-calendar-year-panel{top:34px}.ant-calendar-range .ant-calendar-month-panel .ant-calendar-year-panel{top:0}.ant-calendar-range .ant-calendar-decade-panel-table,.ant-calendar-range .ant-calendar-month-panel-table,.ant-calendar-range .ant-calendar-year-panel-table{height:208px}.ant-calendar-range .ant-calendar-in-range-cell{position:relative;border-radius:0}.ant-calendar-range .ant-calendar-in-range-cell>div{position:relative;z-index:1}.ant-calendar-range .ant-calendar-in-range-cell:before{position:absolute;top:4px;right:0;bottom:4px;left:0;display:block;background:#e6f7ff;border:0;border-radius:0;content:""}.ant-calendar-range .ant-calendar-footer-extra{float:left}div.ant-calendar-range-quick-selector{text-align:left}div.ant-calendar-range-quick-selector>a{margin-right:8px}.ant-calendar-range .ant-calendar-decade-panel-header,.ant-calendar-range .ant-calendar-header,.ant-calendar-range .ant-calendar-month-panel-header,.ant-calendar-range .ant-calendar-year-panel-header{border-bottom:0}.ant-calendar-range .ant-calendar-body,.ant-calendar-range .ant-calendar-decade-panel-body,.ant-calendar-range .ant-calendar-month-panel-body,.ant-calendar-range .ant-calendar-year-panel-body{border-top:1px solid #e8e8e8}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker{top:68px;z-index:2;width:100%;height:207px}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-panel{height:267px;margin-top:-34px}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-inner{height:100%;padding-top:40px;background:none}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-combobox{display:inline-block;height:100%;background-color:#fff;border-top:1px solid #e8e8e8}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-select{height:100%}.ant-calendar-range.ant-calendar-time .ant-calendar-time-picker-select ul{max-height:100%}.ant-calendar-range.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{margin-right:8px}.ant-calendar-range.ant-calendar-time .ant-calendar-today-btn{height:22px;margin:8px 12px;line-height:22px}.ant-calendar-range-with-ranges.ant-calendar-time .ant-calendar-time-picker{height:233px}.ant-calendar-range.ant-calendar-show-time-picker .ant-calendar-body{border-top-color:transparent}.ant-calendar-time-picker{position:absolute;top:40px;width:100%;background-color:#fff}.ant-calendar-time-picker-panel{position:absolute;z-index:1050;width:100%}.ant-calendar-time-picker-inner{position:relative;display:inline-block;width:100%;overflow:hidden;font-size:14px;line-height:1.5;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;outline:none}.ant-calendar-time-picker-column-1,.ant-calendar-time-picker-column-1 .ant-calendar-time-picker-select,.ant-calendar-time-picker-combobox{width:100%}.ant-calendar-time-picker-column-2 .ant-calendar-time-picker-select{width:50%}.ant-calendar-time-picker-column-3 .ant-calendar-time-picker-select{width:33.33%}.ant-calendar-time-picker-column-4 .ant-calendar-time-picker-select{width:25%}.ant-calendar-time-picker-input-wrap{display:none}.ant-calendar-time-picker-select{position:relative;float:left;height:226px;overflow:hidden;font-size:14px;border-right:1px solid #e8e8e8}.ant-calendar-time-picker-select:hover{overflow-y:auto}.ant-calendar-time-picker-select:first-child{margin-left:0;border-left:0}.ant-calendar-time-picker-select:last-child{border-right:0}.ant-calendar-time-picker-select ul{width:100%;max-height:206px;margin:0;padding:0;list-style:none}.ant-calendar-time-picker-select li{width:100%;height:24px;margin:0;line-height:24px;text-align:center;list-style:none;cursor:pointer;-webkit-transition:all .3s;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-calendar-time-picker-select li:last-child:after{display:block;height:202px;content:""}.ant-calendar-time-picker-select li:hover{background:#e6f7ff}.ant-calendar-time-picker-select li:focus{color:#1890ff;font-weight:600;outline:none}li.ant-calendar-time-picker-select-option-selected{font-weight:600;background:#f5f5f5}li.ant-calendar-time-picker-select-option-disabled{color:rgba(0,0,0,.25)}li.ant-calendar-time-picker-select-option-disabled:hover{background:transparent;cursor:not-allowed}.ant-calendar-time .ant-calendar-day-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:34px}.ant-calendar-time .ant-calendar-footer{position:relative;height:auto}.ant-calendar-time .ant-calendar-footer-btn{text-align:right}.ant-calendar-time .ant-calendar-footer .ant-calendar-today-btn{float:left;margin:0}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{display:inline-block;margin-right:8px}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn-disabled{color:rgba(0,0,0,.25)}.ant-calendar-month-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#fff;border-radius:4px;outline:none}.ant-calendar-month-panel>div{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100%}.ant-calendar-month-panel-hidden{display:none}.ant-calendar-month-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative}.ant-calendar-month-panel-header a:hover{color:#40a9ff}.ant-calendar-month-panel-header .ant-calendar-month-panel-century-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-month-select,.ant-calendar-month-panel-header .ant-calendar-month-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-month-panel-header .ant-calendar-month-panel-century-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-month-select-arrow,.ant-calendar-month-panel-header .ant-calendar-month-panel-year-select-arrow{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,"Hiragino Sans GB","Microsoft Yahei","Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn{right:7px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:hover:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:before,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-century-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-decade-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-year-btn:after{display:inline-block}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-prev-month-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn{right:29px;height:100%}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:hover:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after{display:none}.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:after,.ant-calendar-month-panel-header .ant-calendar-month-panel-next-month-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-month-panel-body{-ms-flex:1;flex:1 1}.ant-calendar-month-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-month-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-month-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month,.ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month:hover{color:#fff;background:#1890ff}.ant-calendar-month-panel-cell{text-align:center}.ant-calendar-month-panel-cell-disabled .ant-calendar-month-panel-month,.ant-calendar-month-panel-cell-disabled .ant-calendar-month-panel-month:hover{color:rgba(0,0,0,.25);background:#f5f5f5;cursor:not-allowed}.ant-calendar-month-panel-month{display:inline-block;height:24px;margin:0 auto;padding:0 8px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-calendar-month-panel-month:hover{background:#e6f7ff;cursor:pointer}.ant-calendar-year-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;background:#fff;border-radius:4px;outline:none}.ant-calendar-year-panel>div{display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;height:100%}.ant-calendar-year-panel-hidden{display:none}.ant-calendar-year-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative}.ant-calendar-year-panel-header a:hover{color:#40a9ff}.ant-calendar-year-panel-header .ant-calendar-year-panel-century-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-month-select,.ant-calendar-year-panel-header .ant-calendar-year-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-year-panel-header .ant-calendar-year-panel-century-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-month-select-arrow,.ant-calendar-year-panel-header .ant-calendar-year-panel-year-select-arrow{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,"Hiragino Sans GB","Microsoft Yahei","Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn{right:7px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:hover:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:before,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-century-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-decade-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-year-btn:after{display:inline-block}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-prev-month-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn{right:29px;height:100%}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:hover:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after{display:none}.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:after,.ant-calendar-year-panel-header .ant-calendar-year-panel-next-month-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-year-panel-body{-ms-flex:1;flex:1 1}.ant-calendar-year-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-year-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-year-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-year-panel-cell{text-align:center}.ant-calendar-year-panel-year{display:inline-block;height:24px;margin:0 auto;padding:0 8px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-calendar-year-panel-year:hover{background:#e6f7ff;cursor:pointer}.ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,.ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year:hover{color:#fff;background:#1890ff}.ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,.ant-calendar-year-panel-next-decade-cell .ant-calendar-year-panel-year{color:rgba(0,0,0,.25);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-calendar-decade-panel{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;background:#fff;border-radius:4px;outline:none}.ant-calendar-decade-panel-hidden{display:none}.ant-calendar-decade-panel-header{height:40px;line-height:40px;text-align:center;border-bottom:1px solid #e8e8e8;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;position:relative}.ant-calendar-decade-panel-header a:hover{color:#40a9ff}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-century-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-decade-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-month-select,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-year-select{display:inline-block;padding:0 2px;color:rgba(0,0,0,.85);font-weight:500;line-height:40px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-century-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-decade-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-month-select-arrow,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-year-select-arrow{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn{position:absolute;top:0;display:inline-block;padding:0 5px;color:rgba(0,0,0,.45);font-size:16px;font-family:Arial,"Hiragino Sans GB","Microsoft Yahei","Microsoft Sans Serif",sans-serif;line-height:40px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn{left:7px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-year-btn:after{display:none;position:relative;left:-3px;display:inline-block}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn{right:7px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:hover:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:before,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:before{position:relative;left:3px}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-century-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-decade-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-year-btn:after{display:inline-block}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn{left:29px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-prev-month-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn{right:29px;height:100%}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:before{position:relative;top:-1px;display:inline-block;width:8px;height:8px;vertical-align:middle;border:0 solid #aaa;border-width:1.5px 0 0 1.5px;border-radius:1px;-webkit-transform:rotate(-45deg) scale(.8);-ms-transform:rotate(-45deg) scale(.8);transform:rotate(-45deg) scale(.8);-webkit-transition:all .3s;transition:all .3s;content:""}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:hover:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:hover:before{border-color:rgba(0,0,0,.65)}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after{display:none}.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:after,.ant-calendar-decade-panel-header .ant-calendar-decade-panel-next-month-btn:before{-webkit-transform:rotate(135deg) scale(.8);-ms-transform:rotate(135deg) scale(.8);transform:rotate(135deg) scale(.8)}.ant-calendar-decade-panel-body{-ms-flex:1;flex:1 1}.ant-calendar-decade-panel-footer{border-top:1px solid #e8e8e8}.ant-calendar-decade-panel-footer .ant-calendar-footer-extra{padding:0 12px}.ant-calendar-decade-panel-table{width:100%;height:100%;table-layout:fixed;border-collapse:separate}.ant-calendar-decade-panel-cell{white-space:nowrap;text-align:center}.ant-calendar-decade-panel-decade{display:inline-block;height:24px;margin:0 auto;padding:0 6px;color:rgba(0,0,0,.65);line-height:24px;text-align:center;background:transparent;border-radius:2px;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-calendar-decade-panel-decade:hover{background:#e6f7ff;cursor:pointer}.ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade,.ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade:hover{color:#fff;background:#1890ff}.ant-calendar-decade-panel-last-century-cell .ant-calendar-decade-panel-decade,.ant-calendar-decade-panel-next-century-cell .ant-calendar-decade-panel-decade{color:rgba(0,0,0,.25);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-calendar-month .ant-calendar-month-header-wrap{position:relative;height:288px}.ant-calendar-month .ant-calendar-month-panel,.ant-calendar-month .ant-calendar-year-panel{top:0;height:100%}.ant-calendar-week-number-cell{opacity:.5}.ant-calendar-week-number .ant-calendar-body tr{cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-calendar-week-number .ant-calendar-body tr:hover{background:#e6f7ff}.ant-calendar-week-number .ant-calendar-body tr.ant-calendar-active-week{font-weight:700;background:#bae7ff}.ant-calendar-week-number .ant-calendar-body tr .ant-calendar-selected-day .ant-calendar-date,.ant-calendar-week-number .ant-calendar-body tr .ant-calendar-selected-day:hover .ant-calendar-date{color:rgba(0,0,0,.65);background:transparent}.ant-time-picker-panel{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;z-index:1050;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.ant-time-picker-panel-inner{position:relative;left:-2px;font-size:14px;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-time-picker-panel-input{width:100%;max-width:154px;margin:0;padding:0;line-height:normal;border:0;outline:0;cursor:auto}.ant-time-picker-panel-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-time-picker-panel-input:-ms-input-placeholder{color:#bfbfbf}.ant-time-picker-panel-input::-webkit-input-placeholder{color:#bfbfbf}.ant-time-picker-panel-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-time-picker-panel-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-time-picker-panel-input:placeholder-shown{text-overflow:ellipsis}.ant-time-picker-panel-input-wrap{position:relative;padding:7px 2px 7px 12px;border-bottom:1px solid #e8e8e8}.ant-time-picker-panel-input-invalid{border-color:#f5222d}.ant-time-picker-panel-narrow .ant-time-picker-panel-input-wrap{max-width:112px}.ant-time-picker-panel-select{position:relative;float:left;width:56px;max-height:192px;overflow:hidden;font-size:14px;border-left:1px solid #e8e8e8}.ant-time-picker-panel-select:hover{overflow-y:auto}.ant-time-picker-panel-select:first-child{margin-left:0;border-left:0}.ant-time-picker-panel-select:last-child{border-right:0}.ant-time-picker-panel-select:only-child{width:100%}.ant-time-picker-panel-select ul{width:56px;margin:0;padding:0 0 160px;list-style:none}.ant-time-picker-panel-select li{width:100%;height:32px;margin:0;padding:0 0 0 12px;line-height:32px;text-align:left;list-style:none;cursor:pointer;-webkit-transition:all .3s;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-time-picker-panel-select li:focus{color:#1890ff;font-weight:600;outline:none}.ant-time-picker-panel-select li:hover{background:#e6f7ff}li.ant-time-picker-panel-select-option-selected{font-weight:600;background:#f5f5f5}li.ant-time-picker-panel-select-option-selected:hover{background:#f5f5f5}li.ant-time-picker-panel-select-option-disabled{color:rgba(0,0,0,.25)}li.ant-time-picker-panel-select-option-disabled:hover{background:transparent;cursor:not-allowed}li.ant-time-picker-panel-select-option-disabled:focus{color:rgba(0,0,0,.25);font-weight:inherit}.ant-time-picker-panel-combobox{zoom:1}.ant-time-picker-panel-combobox:after,.ant-time-picker-panel-combobox:before{display:table;content:""}.ant-time-picker-panel-combobox:after{clear:both}.ant-time-picker-panel-addon{padding:8px;border-top:1px solid #e8e8e8}.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-topRight,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-topRight{-webkit-animation-name:antSlideDownIn;animation-name:antSlideDownIn}.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-appear.slide-up-appear-active.ant-time-picker-panel-placement-bottomRight,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-enter.slide-up-enter-active.ant-time-picker-panel-placement-bottomRight{-webkit-animation-name:antSlideUpIn;animation-name:antSlideUpIn}.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-topLeft,.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-topRight{-webkit-animation-name:antSlideDownOut;animation-name:antSlideDownOut}.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-bottomLeft,.ant-time-picker-panel.slide-up-leave.slide-up-leave-active.ant-time-picker-panel-placement-bottomRight{-webkit-animation-name:antSlideUpOut;animation-name:antSlideUpOut}.ant-time-picker{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;font-size:14px;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";width:128px;outline:none;cursor:text;-webkit-transition:opacity .3s;transition:opacity .3s}.ant-time-picker,.ant-time-picker-input{color:rgba(0,0,0,.65);line-height:1.5;position:relative;display:inline-block}.ant-time-picker-input{width:100%;height:32px;padding:4px 11px;font-size:14px;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s}.ant-time-picker-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-time-picker-input:-ms-input-placeholder{color:#bfbfbf}.ant-time-picker-input::-webkit-input-placeholder{color:#bfbfbf}.ant-time-picker-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-time-picker-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-time-picker-input:placeholder-shown{text-overflow:ellipsis}.ant-time-picker-input:focus,.ant-time-picker-input:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-time-picker-input:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-time-picker-input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-time-picker-input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-time-picker-input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-time-picker-input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-time-picker-input-sm{height:24px;padding:1px 7px}.ant-time-picker-input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-time-picker-input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-time-picker-open{opacity:0}.ant-time-picker-clear,.ant-time-picker-icon{position:absolute;top:50%;right:11px;z-index:1;width:14px;height:14px;margin-top:-7px;color:rgba(0,0,0,.25);line-height:14px;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-time-picker-clear .ant-time-picker-clock-icon,.ant-time-picker-icon .ant-time-picker-clock-icon{display:block;color:rgba(0,0,0,.25);line-height:1}.ant-time-picker-clear{z-index:2;background:#fff;opacity:0;pointer-events:none}.ant-time-picker-clear:hover{color:rgba(0,0,0,.45)}.ant-time-picker:hover .ant-time-picker-clear{opacity:1;pointer-events:auto}.ant-time-picker-large .ant-time-picker-input{height:40px;padding:6px 11px;font-size:16px}.ant-time-picker-small .ant-time-picker-input{height:24px;padding:1px 7px}.ant-time-picker-small .ant-time-picker-clear,.ant-time-picker-small .ant-time-picker-icon{right:7px}@media not all and (min-resolution:0.001dpcm){@supports (-webkit-appearance:none) and (stroke-color:transparent){.ant-input{line-height:1.5}}}.ant-tag{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;height:auto;margin:0 8px 0 0;padding:0 7px;font-size:12px;line-height:20px;white-space:nowrap;background:#fafafa;border:1px solid #d9d9d9;border-radius:4px;cursor:default;opacity:1;-webkit-transition:all .3s cubic-bezier(.78,.14,.15,.86);transition:all .3s cubic-bezier(.78,.14,.15,.86)}.ant-tag:hover{opacity:.85}.ant-tag,.ant-tag a,.ant-tag a:hover{color:rgba(0,0,0,.65)}.ant-tag>a:first-child:last-child{display:inline-block;margin:0 -8px;padding:0 8px}.ant-tag .anticon-close{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);margin-left:3px;color:rgba(0,0,0,.45);font-weight:700;cursor:pointer;-webkit-transition:all .3s cubic-bezier(.78,.14,.15,.86);transition:all .3s cubic-bezier(.78,.14,.15,.86)}:root .ant-tag .anticon-close{font-size:12px}.ant-tag .anticon-close:hover{color:rgba(0,0,0,.85)}.ant-tag-has-color{border-color:transparent}.ant-tag-has-color,.ant-tag-has-color .anticon-close,.ant-tag-has-color .anticon-close:hover,.ant-tag-has-color a,.ant-tag-has-color a:hover{color:#fff}.ant-tag-checkable{background-color:transparent;border-color:transparent}.ant-tag-checkable:not(.ant-tag-checkable-checked):hover{color:#1890ff}.ant-tag-checkable-checked,.ant-tag-checkable:active{color:#fff}.ant-tag-checkable-checked{background-color:#1890ff}.ant-tag-checkable:active{background-color:#096dd9}.ant-tag-hidden{display:none}.ant-tag-pink{color:#eb2f96;background:#fff0f6;border-color:#ffadd2}.ant-tag-pink-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-magenta{color:#eb2f96;background:#fff0f6;border-color:#ffadd2}.ant-tag-magenta-inverse{color:#fff;background:#eb2f96;border-color:#eb2f96}.ant-tag-red{color:#f5222d;background:#fff1f0;border-color:#ffa39e}.ant-tag-red-inverse{color:#fff;background:#f5222d;border-color:#f5222d}.ant-tag-volcano{color:#fa541c;background:#fff2e8;border-color:#ffbb96}.ant-tag-volcano-inverse{color:#fff;background:#fa541c;border-color:#fa541c}.ant-tag-orange{color:#fa8c16;background:#fff7e6;border-color:#ffd591}.ant-tag-orange-inverse{color:#fff;background:#fa8c16;border-color:#fa8c16}.ant-tag-yellow{color:#fadb14;background:#feffe6;border-color:#fffb8f}.ant-tag-yellow-inverse{color:#fff;background:#fadb14;border-color:#fadb14}.ant-tag-gold{color:#faad14;background:#fffbe6;border-color:#ffe58f}.ant-tag-gold-inverse{color:#fff;background:#faad14;border-color:#faad14}.ant-tag-cyan{color:#13c2c2;background:#e6fffb;border-color:#87e8de}.ant-tag-cyan-inverse{color:#fff;background:#13c2c2;border-color:#13c2c2}.ant-tag-lime{color:#a0d911;background:#fcffe6;border-color:#eaff8f}.ant-tag-lime-inverse{color:#fff;background:#a0d911;border-color:#a0d911}.ant-tag-green{color:#52c41a;background:#f6ffed;border-color:#b7eb8f}.ant-tag-green-inverse{color:#fff;background:#52c41a;border-color:#52c41a}.ant-tag-blue{color:#1890ff;background:#e6f7ff;border-color:#91d5ff}.ant-tag-blue-inverse{color:#fff;background:#1890ff;border-color:#1890ff}.ant-tag-geekblue{color:#2f54eb;background:#f0f5ff;border-color:#adc6ff}.ant-tag-geekblue-inverse{color:#fff;background:#2f54eb;border-color:#2f54eb}.ant-tag-purple{color:#722ed1;background:#f9f0ff;border-color:#d3adf7}.ant-tag-purple-inverse{color:#fff;background:#722ed1;border-color:#722ed1}.ant-descriptions-title{margin-bottom:20px;color:rgba(0,0,0,.85);font-weight:700;font-size:16px;line-height:1.5}.ant-descriptions-view{width:100%;overflow:hidden;border-radius:4px}.ant-descriptions-view table{width:100%;table-layout:fixed}.ant-descriptions-row>td,.ant-descriptions-row>th{padding-bottom:16px}.ant-descriptions-row:last-child{border-bottom:none}.ant-descriptions-item-label{color:rgba(0,0,0,.85);font-weight:400;font-size:14px;line-height:1.5}.ant-descriptions-item-label:after{position:relative;top:-.5px;margin:0 8px 0 2px;content:" "}.ant-descriptions-item-colon:after{content:":"}.ant-descriptions-item-no-label:after{margin:0;content:""}.ant-descriptions-item-content{display:table-cell;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5}.ant-descriptions-item{padding-bottom:0}.ant-descriptions-item>span{display:inline-block}.ant-descriptions-middle .ant-descriptions-row>td,.ant-descriptions-middle .ant-descriptions-row>th{padding-bottom:12px}.ant-descriptions-small .ant-descriptions-row>td,.ant-descriptions-small .ant-descriptions-row>th{padding-bottom:8px}.ant-descriptions-bordered .ant-descriptions-view{border:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-view>table{table-layout:auto}.ant-descriptions-bordered .ant-descriptions-item-content,.ant-descriptions-bordered .ant-descriptions-item-label{padding:16px 24px;border-right:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-item-content:last-child,.ant-descriptions-bordered .ant-descriptions-item-label:last-child{border-right:none}.ant-descriptions-bordered .ant-descriptions-item-label{background-color:#fafafa}.ant-descriptions-bordered .ant-descriptions-item-label:after{display:none}.ant-descriptions-bordered .ant-descriptions-row{border-bottom:1px solid #e8e8e8}.ant-descriptions-bordered .ant-descriptions-row:last-child{border-bottom:none}.ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-content,.ant-descriptions-bordered.ant-descriptions-middle .ant-descriptions-item-label{padding:12px 24px}.ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-content,.ant-descriptions-bordered.ant-descriptions-small .ant-descriptions-item-label{padding:8px 16px}.ant-divider{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";background:#e8e8e8}.ant-divider,.ant-divider-vertical{position:relative;top:-.06em;display:inline-block;width:1px;height:.9em;margin:0 8px;vertical-align:middle}.ant-divider-horizontal{display:block;clear:both;width:100%;min-width:100%;height:1px;margin:24px 0}.ant-divider-horizontal.ant-divider-with-text-center,.ant-divider-horizontal.ant-divider-with-text-left,.ant-divider-horizontal.ant-divider-with-text-right{display:table;margin:16px 0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;white-space:nowrap;text-align:center;background:transparent}.ant-divider-horizontal.ant-divider-with-text-center:after,.ant-divider-horizontal.ant-divider-with-text-center:before,.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-left:before,.ant-divider-horizontal.ant-divider-with-text-right:after,.ant-divider-horizontal.ant-divider-with-text-right:before{position:relative;top:50%;display:table-cell;width:50%;border-top:1px solid #e8e8e8;-webkit-transform:translateY(50%);-ms-transform:translateY(50%);transform:translateY(50%);content:""}.ant-divider-horizontal.ant-divider-with-text-left .ant-divider-inner-text,.ant-divider-horizontal.ant-divider-with-text-right .ant-divider-inner-text{display:inline-block;padding:0 10px}.ant-divider-horizontal.ant-divider-with-text-left:before{top:50%;width:5%}.ant-divider-horizontal.ant-divider-with-text-left:after,.ant-divider-horizontal.ant-divider-with-text-right:before{top:50%;width:95%}.ant-divider-horizontal.ant-divider-with-text-right:after{top:50%;width:5%}.ant-divider-inner-text{display:inline-block;padding:0 24px}.ant-divider-dashed{background:none;border:dashed #e8e8e8;border-width:1px 0 0}.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed{border-top:0}.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-center.ant-divider-dashed:before,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-left.ant-divider-dashed:before,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed:after,.ant-divider-horizontal.ant-divider-with-text-right.ant-divider-dashed:before{border-style:dashed none none}.ant-divider-vertical.ant-divider-dashed{border-width:0 0 0 1px}.ant-drawer{position:fixed;z-index:1000;width:0;height:100%;-webkit-transition:height 0s ease .3s,width 0s ease .3s,-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:height 0s ease .3s,width 0s ease .3s,-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1),height 0s ease .3s,width 0s ease .3s;transition:transform .3s cubic-bezier(.7,.3,.1,1),height 0s ease .3s,width 0s ease .3s,-webkit-transform .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer>*{-webkit-transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1),-webkit-box-shadow .3s cubic-bezier(.7,.3,.1,1);transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1),-webkit-box-shadow .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1),box-shadow .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1),box-shadow .3s cubic-bezier(.7,.3,.1,1),-webkit-transform .3s cubic-bezier(.7,.3,.1,1),-webkit-box-shadow .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-content-wrapper{position:absolute}.ant-drawer .ant-drawer-content{width:100%;height:100%}.ant-drawer-left,.ant-drawer-right{top:0;width:0;height:100%}.ant-drawer-left .ant-drawer-content-wrapper,.ant-drawer-right .ant-drawer-content-wrapper{height:100%}.ant-drawer-left.ant-drawer-open,.ant-drawer-right.ant-drawer-open{width:100%;-webkit-transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1),-webkit-transform .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-left.ant-drawer-open.no-mask,.ant-drawer-right.ant-drawer-open.no-mask{width:0}.ant-drawer-left.ant-drawer-open .ant-drawer-content-wrapper{-webkit-box-shadow:2px 0 8px rgba(0,0,0,.15);box-shadow:2px 0 8px rgba(0,0,0,.15)}.ant-drawer-right,.ant-drawer-right .ant-drawer-content-wrapper{right:0}.ant-drawer-right.ant-drawer-open .ant-drawer-content-wrapper{-webkit-box-shadow:-2px 0 8px rgba(0,0,0,.15);box-shadow:-2px 0 8px rgba(0,0,0,.15)}.ant-drawer-right.ant-drawer-open.no-mask{right:1px;-webkit-transform:translateX(1px);-ms-transform:translateX(1px);transform:translateX(1px)}.ant-drawer-bottom,.ant-drawer-top{left:0;width:100%;height:0%}.ant-drawer-bottom .ant-drawer-content-wrapper,.ant-drawer-top .ant-drawer-content-wrapper{width:100%}.ant-drawer-bottom.ant-drawer-open,.ant-drawer-top.ant-drawer-open{height:100%;-webkit-transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:-webkit-transform .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1);transition:transform .3s cubic-bezier(.7,.3,.1,1),-webkit-transform .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-bottom.ant-drawer-open.no-mask,.ant-drawer-top.ant-drawer-open.no-mask{height:0%}.ant-drawer-top{top:0}.ant-drawer-top.ant-drawer-open .ant-drawer-content-wrapper{-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-drawer-bottom,.ant-drawer-bottom .ant-drawer-content-wrapper{bottom:0}.ant-drawer-bottom.ant-drawer-open .ant-drawer-content-wrapper{-webkit-box-shadow:0 -2px 8px rgba(0,0,0,.15);box-shadow:0 -2px 8px rgba(0,0,0,.15)}.ant-drawer-bottom.ant-drawer-open.no-mask{bottom:1px;-webkit-transform:translateY(1px);-ms-transform:translateY(1px);transform:translateY(1px)}.ant-drawer.ant-drawer-open .ant-drawer-mask{height:100%;opacity:1;-webkit-transition:none;transition:none;-webkit-animation:antdDrawerFadeIn .3s cubic-bezier(.7,.3,.1,1);animation:antdDrawerFadeIn .3s cubic-bezier(.7,.3,.1,1)}.ant-drawer-title{margin:0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:22px}.ant-drawer-content{position:relative;z-index:1;overflow:auto;background-color:#fff;background-clip:padding-box;border:0}.ant-drawer-close{position:absolute;top:0;right:0;z-index:10;display:block;width:56px;height:56px;padding:0;color:rgba(0,0,0,.45);font-weight:700;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-decoration:none;background:transparent;border:0;outline:0;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;text-rendering:auto}.ant-drawer-close:focus,.ant-drawer-close:hover{color:rgba(0,0,0,.75);text-decoration:none}.ant-drawer-header{position:relative;padding:16px 24px;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0}.ant-drawer-header,.ant-drawer-header-no-title{color:rgba(0,0,0,.65);background:#fff}.ant-drawer-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}.ant-drawer-wrapper-body{height:100%;overflow:auto}.ant-drawer-mask{position:absolute;top:0;left:0;width:100%;height:0;background-color:rgba(0,0,0,.45);opacity:0;filter:alpha(opacity=45);-webkit-transition:opacity .3s linear,height 0s ease .3s;transition:opacity .3s linear,height 0s ease .3s}.ant-drawer-open-content{-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15)}@-webkit-keyframes antdDrawerFadeIn{0%{opacity:0}to{opacity:1}}@keyframes antdDrawerFadeIn{0%{opacity:0}to{opacity:1}}.ant-form{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-form legend{display:block;width:100%;margin-bottom:20px;padding:0;color:rgba(0,0,0,.45);font-size:16px;line-height:inherit;border:0;border-bottom:1px solid #d9d9d9}.ant-form label{font-size:14px}.ant-form input[type=search]{-webkit-box-sizing:border-box;box-sizing:border-box}.ant-form input[type=checkbox],.ant-form input[type=radio]{line-height:normal}.ant-form input[type=file]{display:block}.ant-form input[type=range]{display:block;width:100%}.ant-form select[multiple],.ant-form select[size]{height:auto}.ant-form input[type=checkbox]:focus,.ant-form input[type=file]:focus,.ant-form input[type=radio]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.ant-form output{display:block;padding-top:15px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5}.ant-form-item-required:before{display:inline-block;margin-right:4px;color:#f5222d;font-size:14px;font-family:SimSun,sans-serif;line-height:1;content:"*"}.ant-form-hide-required-mark .ant-form-item-required:before{display:none}.ant-form-item-label>label{color:rgba(0,0,0,.85)}.ant-form-item-label>label:after{content:":";position:relative;top:-.5px;margin:0 8px 0 2px}.ant-form-item-label>label.ant-form-item-no-colon:after{content:" "}.ant-form-item{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";margin:0 0 24px;vertical-align:top}.ant-form-item label{position:relative}.ant-form-item label>.anticon{font-size:14px;vertical-align:top}.ant-form-item-control{position:relative;line-height:40px;zoom:1}.ant-form-item-control:after,.ant-form-item-control:before{display:table;content:""}.ant-form-item-control:after{clear:both}.ant-form-item-children{position:relative}.ant-form-item-with-help{margin-bottom:5px}.ant-form-item-label{display:inline-block;overflow:hidden;line-height:39.9999px;white-space:nowrap;text-align:right;vertical-align:middle}.ant-form-item-label-left{text-align:left}.ant-form-item .ant-switch{margin:2px 0 4px}.ant-form-explain,.ant-form-extra{clear:both;min-height:22px;margin-top:-2px;color:rgba(0,0,0,.45);font-size:14px;line-height:1.5;-webkit-transition:color .3s cubic-bezier(.215,.61,.355,1);transition:color .3s cubic-bezier(.215,.61,.355,1)}.ant-form-explain{margin-bottom:-1px}.ant-form-extra{padding-top:4px}.ant-form-text{display:inline-block;padding-right:8px}.ant-form-split{display:block;text-align:center}form .has-feedback .ant-input{padding-right:30px}form .has-feedback .ant-input-affix-wrapper .ant-input-suffix{padding-right:18px}form .has-feedback .ant-input-affix-wrapper .ant-input{padding-right:49px}form .has-feedback .ant-input-affix-wrapper.ant-input-affix-wrapper-input-with-clear-btn .ant-input{padding-right:68px}form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-arrow,form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-selection__clear,form .has-feedback>.ant-select .ant-select-arrow,form .has-feedback>.ant-select .ant-select-selection__clear{right:28px}form .has-feedback :not(.ant-input-group-addon)>.ant-select .ant-select-selection-selected-value,form .has-feedback>.ant-select .ant-select-selection-selected-value{padding-right:42px}form .has-feedback .ant-cascader-picker-arrow{margin-right:17px}form .has-feedback .ant-calendar-picker-clear,form .has-feedback .ant-calendar-picker-icon,form .has-feedback .ant-cascader-picker-clear,form .has-feedback .ant-input-search:not(.ant-input-search-enter-button) .ant-input-suffix,form .has-feedback .ant-time-picker-clear,form .has-feedback .ant-time-picker-icon{right:28px}form .ant-mentions,form textarea.ant-input{height:auto;margin-bottom:4px}form .ant-upload{background:transparent}form input[type=checkbox],form input[type=radio]{width:14px;height:14px}form .ant-checkbox-inline,form .ant-radio-inline{display:inline-block;margin-left:8px;font-weight:400;vertical-align:middle;cursor:pointer}form .ant-checkbox-inline:first-child,form .ant-radio-inline:first-child{margin-left:0}form .ant-checkbox-vertical,form .ant-radio-vertical{display:block}form .ant-checkbox-vertical+.ant-checkbox-vertical,form .ant-radio-vertical+.ant-radio-vertical{margin-left:0}form .ant-input-number+.ant-form-text{margin-left:8px}form .ant-input-number-handler-wrap{z-index:2}form .ant-cascader-picker,form .ant-select{width:100%}form .ant-input-group .ant-cascader-picker,form .ant-input-group .ant-select{width:auto}form .ant-input-group-wrapper,form :not(.ant-input-group-wrapper)>.ant-input-group{display:inline-block;vertical-align:middle}form:not(.ant-form-vertical) .ant-input-group-wrapper,form:not(.ant-form-vertical) :not(.ant-input-group-wrapper)>.ant-input-group{position:relative;top:-1px}.ant-col-24.ant-form-item-label,.ant-col-xl-24.ant-form-item-label,.ant-form-vertical .ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-24.ant-form-item-label label:after,.ant-col-xl-24.ant-form-item-label label:after,.ant-form-vertical .ant-form-item-label label:after{display:none}.ant-form-vertical .ant-form-item{padding-bottom:8px}.ant-form-vertical .ant-form-item-control{line-height:1.5}.ant-form-vertical .ant-form-explain{margin-top:2px;margin-bottom:-5px}.ant-form-vertical .ant-form-extra{margin-top:2px;margin-bottom:-4px}@media (max-width:575px){.ant-form-item-control-wrapper,.ant-form-item-label{display:block;width:100%}.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-form-item-label label:after{display:none}.ant-col-xs-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-xs-24.ant-form-item-label label:after{display:none}}@media (max-width:767px){.ant-col-sm-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-sm-24.ant-form-item-label label:after{display:none}}@media (max-width:991px){.ant-col-md-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-md-24.ant-form-item-label label:after{display:none}}@media (max-width:1199px){.ant-col-lg-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-lg-24.ant-form-item-label label:after{display:none}}@media (max-width:1599px){.ant-col-xl-24.ant-form-item-label{display:block;margin:0;padding:0 0 8px;line-height:1.5;white-space:normal;text-align:left}.ant-col-xl-24.ant-form-item-label label:after{display:none}}.ant-form-inline .ant-form-item{display:inline-block;margin-right:16px;margin-bottom:0}.ant-form-inline .ant-form-item-with-help{margin-bottom:24px}.ant-form-inline .ant-form-item>.ant-form-item-control-wrapper,.ant-form-inline .ant-form-item>.ant-form-item-label{display:inline-block;vertical-align:top}.ant-form-inline .ant-form-text,.ant-form-inline .has-feedback{display:inline-block}.has-error.has-feedback .ant-form-item-children-icon,.has-success.has-feedback .ant-form-item-children-icon,.has-warning.has-feedback .ant-form-item-children-icon,.is-validating.has-feedback .ant-form-item-children-icon{position:absolute;top:50%;right:0;z-index:1;width:32px;height:20px;margin-top:-10px;font-size:14px;line-height:20px;text-align:center;visibility:visible;-webkit-animation:zoomIn .3s cubic-bezier(.12,.4,.29,1.46);animation:zoomIn .3s cubic-bezier(.12,.4,.29,1.46);pointer-events:none}.has-error.has-feedback .ant-form-item-children-icon svg,.has-success.has-feedback .ant-form-item-children-icon svg,.has-warning.has-feedback .ant-form-item-children-icon svg,.is-validating.has-feedback .ant-form-item-children-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.has-success.has-feedback .ant-form-item-children-icon{color:#52c41a;-webkit-animation-name:diffZoomIn1!important;animation-name:diffZoomIn1!important}.has-warning .ant-form-explain,.has-warning .ant-form-split{color:#faad14}.has-warning .ant-input,.has-warning .ant-input:hover{background-color:#fff;border-color:#faad14}.has-warning .ant-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input:not([disabled]):hover{border-color:#faad14}.has-warning .ant-calendar-picker-open .ant-calendar-picker-input{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-affix-wrapper .ant-input,.has-warning .ant-input-affix-wrapper .ant-input:hover{background-color:#fff;border-color:#faad14}.has-warning .ant-input-affix-wrapper .ant-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#faad14}.has-warning .ant-input-prefix{color:#faad14}.has-warning .ant-input-group-addon{color:#faad14;background-color:#fff;border-color:#faad14}.has-warning .has-feedback{color:#faad14}.has-warning.has-feedback .ant-form-item-children-icon{color:#faad14;-webkit-animation-name:diffZoomIn3!important;animation-name:diffZoomIn3!important}.has-warning .ant-select-selection,.has-warning .ant-select-selection:hover{border-color:#faad14}.has-warning .ant-select-focused .ant-select-selection,.has-warning .ant-select-open .ant-select-selection{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-calendar-picker-icon:after,.has-warning .ant-cascader-picker-arrow,.has-warning .ant-picker-icon:after,.has-warning .ant-select-arrow,.has-warning .ant-time-picker-icon:after{color:#faad14}.has-warning .ant-input-number,.has-warning .ant-time-picker-input{border-color:#faad14}.has-warning .ant-input-number-focused,.has-warning .ant-input-number:focus,.has-warning .ant-time-picker-input-focused,.has-warning .ant-time-picker-input:focus{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-input-number:not([disabled]):hover,.has-warning .ant-time-picker-input:not([disabled]):hover{border-color:#faad14}.has-warning .ant-cascader-picker:focus .ant-cascader-input{border-color:#ffc53d;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(250,173,20,.2);box-shadow:0 0 0 2px rgba(250,173,20,.2)}.has-warning .ant-cascader-picker:hover .ant-cascader-input{border-color:#faad14}.has-error .ant-form-explain,.has-error .ant-form-split{color:#f5222d}.has-error .ant-input,.has-error .ant-input:hover{background-color:#fff;border-color:#f5222d}.has-error .ant-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input:not([disabled]):hover{border-color:#f5222d}.has-error .ant-calendar-picker-open .ant-calendar-picker-input{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-affix-wrapper .ant-input,.has-error .ant-input-affix-wrapper .ant-input:hover{background-color:#fff;border-color:#f5222d}.has-error .ant-input-affix-wrapper .ant-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:#f5222d}.has-error .ant-input-prefix{color:#f5222d}.has-error .ant-input-group-addon{color:#f5222d;background-color:#fff;border-color:#f5222d}.has-error .has-feedback{color:#f5222d}.has-error.has-feedback .ant-form-item-children-icon{color:#f5222d;-webkit-animation-name:diffZoomIn2!important;animation-name:diffZoomIn2!important}.has-error .ant-select-selection,.has-error .ant-select-selection:hover{border-color:#f5222d}.has-error .ant-select-focused .ant-select-selection,.has-error .ant-select-open .ant-select-selection{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-select.ant-select-auto-complete .ant-input:focus{border-color:#f5222d}.has-error .ant-input-group-addon .ant-select-selection{border-color:transparent;-webkit-box-shadow:none;box-shadow:none}.has-error .ant-calendar-picker-icon:after,.has-error .ant-cascader-picker-arrow,.has-error .ant-picker-icon:after,.has-error .ant-select-arrow,.has-error .ant-time-picker-icon:after{color:#f5222d}.has-error .ant-input-number,.has-error .ant-time-picker-input{border-color:#f5222d}.has-error .ant-input-number-focused,.has-error .ant-input-number:focus,.has-error .ant-time-picker-input-focused,.has-error .ant-time-picker-input:focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-input-number:not([disabled]):hover,.has-error .ant-mention-wrapper .ant-mention-editor,.has-error .ant-mention-wrapper .ant-mention-editor:not([disabled]):hover,.has-error .ant-time-picker-input:not([disabled]):hover{border-color:#f5222d}.has-error .ant-cascader-picker:focus .ant-cascader-input,.has-error .ant-mention-wrapper.ant-mention-active:not([disabled]) .ant-mention-editor,.has-error .ant-mention-wrapper .ant-mention-editor:not([disabled]):focus{border-color:#ff4d4f;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(245,34,45,.2);box-shadow:0 0 0 2px rgba(245,34,45,.2)}.has-error .ant-cascader-picker:hover .ant-cascader-input,.has-error .ant-transfer-list{border-color:#f5222d}.has-error .ant-transfer-list-search:not([disabled]){border-color:#d9d9d9}.has-error .ant-transfer-list-search:not([disabled]):hover{border-color:#40a9ff;border-right-width:1px!important}.has-error .ant-transfer-list-search:not([disabled]):focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.is-validating.has-feedback .ant-form-item-children-icon{display:inline-block;color:#1890ff}.ant-advanced-search-form .ant-form-item{margin-bottom:24px}.ant-advanced-search-form .ant-form-item-with-help{margin-bottom:5px}.show-help-appear,.show-help-enter,.show-help-leave{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-play-state:paused;animation-play-state:paused}.show-help-appear.show-help-appear-active,.show-help-enter.show-help-enter-active{-webkit-animation-name:antShowHelpIn;animation-name:antShowHelpIn;-webkit-animation-play-state:running;animation-play-state:running}.show-help-leave.show-help-leave-active{-webkit-animation-name:antShowHelpOut;animation-name:antShowHelpOut;-webkit-animation-play-state:running;animation-play-state:running;pointer-events:none}.show-help-appear,.show-help-enter{opacity:0}.show-help-appear,.show-help-enter,.show-help-leave{-webkit-animation-timing-function:cubic-bezier(.645,.045,.355,1);animation-timing-function:cubic-bezier(.645,.045,.355,1)}@-webkit-keyframes antShowHelpIn{0%{-webkit-transform:translateY(-5px);transform:translateY(-5px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes antShowHelpIn{0%{-webkit-transform:translateY(-5px);transform:translateY(-5px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@-webkit-keyframes antShowHelpOut{to{-webkit-transform:translateY(-5px);transform:translateY(-5px);opacity:0}}@keyframes antShowHelpOut{to{-webkit-transform:translateY(-5px);transform:translateY(-5px);opacity:0}}@-webkit-keyframes diffZoomIn1{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes diffZoomIn1{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes diffZoomIn2{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes diffZoomIn2{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes diffZoomIn3{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}@keyframes diffZoomIn3{0%{-webkit-transform:scale(0);transform:scale(0)}to{-webkit-transform:scale(1);transform:scale(1)}}.ant-input-number{-webkit-box-sizing:border-box;box-sizing:border-box;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;-webkit-transition:all .3s;transition:all .3s;display:inline-block;width:90px;margin:0;padding:0;border:1px solid #d9d9d9;border-radius:4px}.ant-input-number::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input-number:-ms-input-placeholder{color:#bfbfbf}.ant-input-number::-webkit-input-placeholder{color:#bfbfbf}.ant-input-number:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input-number:-ms-input-placeholder{text-overflow:ellipsis}.ant-input-number:placeholder-shown{text-overflow:ellipsis}.ant-input-number:focus{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-input-number[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-number[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-input-number{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-input-number-lg{height:40px;padding:6px 11px}.ant-input-number-sm{height:24px;padding:1px 7px}.ant-input-number-handler{position:relative;display:block;width:100%;height:50%;overflow:hidden;color:rgba(0,0,0,.45);font-weight:700;line-height:0;text-align:center;-webkit-transition:all .1s linear;transition:all .1s linear}.ant-input-number-handler:active{background:#f4f4f4}.ant-input-number-handler:hover .ant-input-number-handler-down-inner,.ant-input-number-handler:hover .ant-input-number-handler-up-inner{color:#40a9ff}.ant-input-number-handler-down-inner,.ant-input-number-handler-up-inner{display:inline-block;color:inherit;font-style:normal;line-height:0;text-align:center;text-transform:none;vertical-align:-.125em;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;position:absolute;right:4px;width:12px;height:12px;color:rgba(0,0,0,.45);line-height:12px;-webkit-transition:all .1s linear;transition:all .1s linear;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-input-number-handler-down-inner>*,.ant-input-number-handler-up-inner>*{line-height:1}.ant-input-number-handler-down-inner svg,.ant-input-number-handler-up-inner svg{display:inline-block}.ant-input-number-handler-down-inner:before,.ant-input-number-handler-up-inner:before{display:none}.ant-input-number-handler-down-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-down-inner .ant-input-number-handler-up-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-down-inner-icon,.ant-input-number-handler-up-inner .ant-input-number-handler-up-inner-icon{display:block}.ant-input-number-focused,.ant-input-number:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-input-number-focused{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-input-number-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-input-number-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-input-number-disabled .ant-input-number-input{cursor:not-allowed}.ant-input-number-disabled .ant-input-number-handler-wrap{display:none}.ant-input-number-input{width:100%;height:30px;padding:0 11px;text-align:left;background-color:transparent;border:0;border-radius:4px;outline:0;-webkit-transition:all .3s linear;transition:all .3s linear;-moz-appearance:textfield!important}.ant-input-number-input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-input-number-input:-ms-input-placeholder{color:#bfbfbf}.ant-input-number-input::-webkit-input-placeholder{color:#bfbfbf}.ant-input-number-input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-input-number-input:-ms-input-placeholder{text-overflow:ellipsis}.ant-input-number-input:placeholder-shown{text-overflow:ellipsis}.ant-input-number-input[type=number]::-webkit-inner-spin-button,.ant-input-number-input[type=number]::-webkit-outer-spin-button{margin:0;-webkit-appearance:none}.ant-input-number-lg{padding:0;font-size:16px}.ant-input-number-lg input{height:38px}.ant-input-number-sm{padding:0}.ant-input-number-sm input{height:22px;padding:0 7px}.ant-input-number-handler-wrap{position:absolute;top:0;right:0;width:22px;height:100%;background:#fff;border-left:1px solid #d9d9d9;border-radius:0 4px 4px 0;opacity:0;-webkit-transition:opacity .24s linear .1s;transition:opacity .24s linear .1s}.ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-down-inner,.ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-up-inner{display:inline-block;font-size:12px;font-size:7px\9;-webkit-transform:scale(.58333333) rotate(0deg);-ms-transform:scale(.58333333) rotate(0deg);transform:scale(.58333333) rotate(0deg);min-width:auto;margin-right:0}:root .ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-down-inner,:root .ant-input-number-handler-wrap .ant-input-number-handler .ant-input-number-handler-up-inner{font-size:12px}.ant-input-number-handler-wrap:hover .ant-input-number-handler{height:40%}.ant-input-number:hover .ant-input-number-handler-wrap{opacity:1}.ant-input-number-handler-up{border-top-right-radius:4px;cursor:pointer}.ant-input-number-handler-up-inner{top:50%;margin-top:-5px;text-align:center}.ant-input-number-handler-up:hover{height:60%!important}.ant-input-number-handler-down{top:0;border-top:1px solid #d9d9d9;border-bottom-right-radius:4px;cursor:pointer}.ant-input-number-handler-down-inner{top:50%;margin-top:-6px;text-align:center}.ant-input-number-handler-down:hover{height:60%!important}.ant-input-number-handler-down-disabled,.ant-input-number-handler-up-disabled{cursor:not-allowed}.ant-input-number-handler-down-disabled:hover .ant-input-number-handler-down-inner,.ant-input-number-handler-up-disabled:hover .ant-input-number-handler-up-inner{color:rgba(0,0,0,.25)}.ant-layout{display:-ms-flexbox;display:flex;-ms-flex:auto;flex:auto;-ms-flex-direction:column;flex-direction:column;min-height:0;background:#f0f2f5}.ant-layout,.ant-layout *{-webkit-box-sizing:border-box;box-sizing:border-box}.ant-layout.ant-layout-has-sider{-ms-flex-direction:row;flex-direction:row}.ant-layout.ant-layout-has-sider>.ant-layout,.ant-layout.ant-layout-has-sider>.ant-layout-content{overflow-x:hidden}.ant-layout-footer,.ant-layout-header{-ms-flex:0 0 auto;flex:0 0 auto}.ant-layout-header{height:64px;padding:0 50px;line-height:64px;background:#001529}.ant-layout-footer{padding:24px 50px;color:rgba(0,0,0,.65);font-size:14px;background:#f0f2f5}.ant-layout-content{-ms-flex:auto;flex:auto;min-height:0}.ant-layout-sider{position:relative;min-width:0;background:#001529;-webkit-transition:all .2s;transition:all .2s}.ant-layout-sider-children{height:100%;margin-top:-.1px;padding-top:.1px}.ant-layout-sider-has-trigger{padding-bottom:48px}.ant-layout-sider-right{-ms-flex-order:1;order:1}.ant-layout-sider-trigger{position:fixed;bottom:0;z-index:1;height:48px;color:#fff;line-height:48px;text-align:center;background:#002140;cursor:pointer;-webkit-transition:all .2s;transition:all .2s}.ant-layout-sider-zero-width>*{overflow:hidden}.ant-layout-sider-zero-width-trigger{position:absolute;top:64px;right:-36px;z-index:1;width:36px;height:42px;color:#fff;font-size:18px;line-height:42px;text-align:center;background:#001529;border-radius:0 4px 4px 0;cursor:pointer;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-layout-sider-zero-width-trigger:hover{background:#192c3e}.ant-layout-sider-zero-width-trigger-right{left:-36px;border-radius:4px 0 0 4px}.ant-layout-sider-light{background:#fff}.ant-layout-sider-light .ant-layout-sider-trigger,.ant-layout-sider-light .ant-layout-sider-zero-width-trigger{color:rgba(0,0,0,.65);background:#fff}.ant-list{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative}.ant-list *{outline:none}.ant-list-pagination{margin-top:24px;text-align:right}.ant-list-pagination .ant-pagination-options{text-align:left}.ant-list-more{margin-top:12px;text-align:center}.ant-list-more button{padding-right:32px;padding-left:32px}.ant-list-spin{min-height:40px;text-align:center}.ant-list-empty-text{padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center}.ant-list-items{margin:0;padding:0;list-style:none}.ant-list-item{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;-ms-flex-pack:justify;justify-content:space-between;padding:12px 0}.ant-list-item-content{color:rgba(0,0,0,.65)}.ant-list-item-meta{display:-ms-flexbox;display:flex;-ms-flex:1;flex:1 1;-ms-flex-align:start;align-items:flex-start;font-size:0}.ant-list-item-meta-avatar{margin-right:16px}.ant-list-item-meta-content{-ms-flex:1 0;flex:1 0}.ant-list-item-meta-title{margin-bottom:4px;color:rgba(0,0,0,.65);font-size:14px;line-height:22px}.ant-list-item-meta-title>a{color:rgba(0,0,0,.65);-webkit-transition:all .3s;transition:all .3s}.ant-list-item-meta-title>a:hover{color:#1890ff}.ant-list-item-meta-description{color:rgba(0,0,0,.45);font-size:14px;line-height:22px}.ant-list-item-action{-ms-flex:0 0 auto;flex:0 0 auto;margin-left:48px;padding:0;font-size:0;list-style:none}.ant-list-item-action>li{position:relative;display:inline-block;padding:0 8px;color:rgba(0,0,0,.45);font-size:14px;line-height:22px;text-align:center;cursor:pointer}.ant-list-item-action>li:first-child{padding-left:0}.ant-list-item-action-split{position:absolute;top:50%;right:0;width:1px;height:14px;margin-top:-7px;background-color:#e8e8e8}.ant-list-footer,.ant-list-header{background:transparent}.ant-list-footer,.ant-list-header{padding-top:12px;padding-bottom:12px}.ant-list-empty{padding:16px 0;color:rgba(0,0,0,.45);font-size:12px;text-align:center}.ant-list-split .ant-list-item{border-bottom:1px solid #e8e8e8}.ant-list-split .ant-list-item:last-child{border-bottom:none}.ant-list-split .ant-list-header{border-bottom:1px solid #e8e8e8}.ant-list-loading .ant-list-spin-nested-loading{min-height:32px}.ant-list-something-after-last-item .ant-spin-container>.ant-list-items>.ant-list-item:last-child{border-bottom:1px solid #e8e8e8}.ant-list-lg .ant-list-item{padding-top:16px;padding-bottom:16px}.ant-list-sm .ant-list-item{padding-top:8px;padding-bottom:8px}.ant-list-vertical .ant-list-item{-ms-flex-align:initial;align-items:normal}.ant-list-vertical .ant-list-item-main{display:block;-ms-flex:1;flex:1 1}.ant-list-vertical .ant-list-item-extra{margin-left:40px}.ant-list-vertical .ant-list-item-meta{margin-bottom:16px}.ant-list-vertical .ant-list-item-meta-title{margin-bottom:12px;color:rgba(0,0,0,.85);font-size:16px;line-height:24px}.ant-list-vertical .ant-list-item-action{margin-top:16px;margin-left:auto}.ant-list-vertical .ant-list-item-action>li{padding:0 16px}.ant-list-vertical .ant-list-item-action>li:first-child{padding-left:0}.ant-list-grid .ant-col>.ant-list-item{display:block;max-width:100%;margin-bottom:16px;padding-top:0;padding-bottom:0;border-bottom:none}.ant-list-item-no-flex{display:block}.ant-list:not(.ant-list-vertical) .ant-list-item-no-flex .ant-list-item-action{float:right}.ant-list-bordered{border:1px solid #d9d9d9;border-radius:4px}.ant-list-bordered .ant-list-footer,.ant-list-bordered .ant-list-header,.ant-list-bordered .ant-list-item{padding-right:24px;padding-left:24px}.ant-list-bordered .ant-list-item{border-bottom:1px solid #e8e8e8}.ant-list-bordered .ant-list-pagination{margin:16px 24px}.ant-list-bordered.ant-list-sm .ant-list-item{padding-right:16px;padding-left:16px}.ant-list-bordered.ant-list-sm .ant-list-footer,.ant-list-bordered.ant-list-sm .ant-list-header{padding:8px 16px}.ant-list-bordered.ant-list-lg .ant-list-footer,.ant-list-bordered.ant-list-lg .ant-list-header{padding:16px 24px}@media screen and (max-width:768px){.ant-list-item-action,.ant-list-vertical .ant-list-item-extra{margin-left:24px}}@media screen and (max-width:576px){.ant-list-item{-ms-flex-wrap:wrap;flex-wrap:wrap}.ant-list-item-action{margin-left:12px}.ant-list-vertical .ant-list-item{-ms-flex-wrap:wrap-reverse;flex-wrap:wrap-reverse}.ant-list-vertical .ant-list-item-main{min-width:220px}.ant-list-vertical .ant-list-item-extra{margin:auto auto 16px}}.ant-spin{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;display:none;color:#1890ff;text-align:center;vertical-align:middle;opacity:0;-webkit-transition:-webkit-transform .3s cubic-bezier(.78,.14,.15,.86);transition:-webkit-transform .3s cubic-bezier(.78,.14,.15,.86);transition:transform .3s cubic-bezier(.78,.14,.15,.86);transition:transform .3s cubic-bezier(.78,.14,.15,.86),-webkit-transform .3s cubic-bezier(.78,.14,.15,.86)}.ant-spin-spinning{position:static;display:inline-block;opacity:1}.ant-spin-nested-loading{position:relative}.ant-spin-nested-loading>div>.ant-spin{position:absolute;top:0;left:0;z-index:4;display:block;width:100%;height:100%;max-height:400px}.ant-spin-nested-loading>div>.ant-spin .ant-spin-dot{position:absolute;top:50%;left:50%;margin:-10px}.ant-spin-nested-loading>div>.ant-spin .ant-spin-text{position:absolute;top:50%;width:100%;padding-top:5px;text-shadow:0 1px 2px #fff}.ant-spin-nested-loading>div>.ant-spin.ant-spin-show-text .ant-spin-dot{margin-top:-20px}.ant-spin-nested-loading>div>.ant-spin-sm .ant-spin-dot{margin:-7px}.ant-spin-nested-loading>div>.ant-spin-sm .ant-spin-text{padding-top:2px}.ant-spin-nested-loading>div>.ant-spin-sm.ant-spin-show-text .ant-spin-dot{margin-top:-17px}.ant-spin-nested-loading>div>.ant-spin-lg .ant-spin-dot{margin:-16px}.ant-spin-nested-loading>div>.ant-spin-lg .ant-spin-text{padding-top:11px}.ant-spin-nested-loading>div>.ant-spin-lg.ant-spin-show-text .ant-spin-dot{margin-top:-26px}.ant-spin-container{position:relative;-webkit-transition:opacity .3s;transition:opacity .3s}.ant-spin-container:after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:10;display:none\9;width:100%;height:100%;background:#fff;opacity:0;-webkit-transition:all .3s;transition:all .3s;content:"";pointer-events:none}.ant-spin-blur{clear:both;overflow:hidden;opacity:.5;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none}.ant-spin-blur:after{opacity:.4;pointer-events:auto}.ant-spin-tip{color:rgba(0,0,0,.45)}.ant-spin-dot{position:relative;display:inline-block;font-size:20px;width:1em;height:1em}.ant-spin-dot-item{position:absolute;display:block;width:9px;height:9px;background-color:#1890ff;border-radius:100%;-webkit-transform:scale(.75);-ms-transform:scale(.75);transform:scale(.75);-webkit-transform-origin:50% 50%;-ms-transform-origin:50% 50%;transform-origin:50% 50%;opacity:.3;-webkit-animation:antSpinMove 1s linear infinite alternate;animation:antSpinMove 1s linear infinite alternate}.ant-spin-dot-item:first-child{top:0;left:0}.ant-spin-dot-item:nth-child(2){top:0;right:0;-webkit-animation-delay:.4s;animation-delay:.4s}.ant-spin-dot-item:nth-child(3){right:0;bottom:0;-webkit-animation-delay:.8s;animation-delay:.8s}.ant-spin-dot-item:nth-child(4){bottom:0;left:0;-webkit-animation-delay:1.2s;animation-delay:1.2s}.ant-spin-dot-spin{-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);-webkit-animation:antRotate 1.2s linear infinite;animation:antRotate 1.2s linear infinite}.ant-spin-sm .ant-spin-dot{font-size:14px}.ant-spin-sm .ant-spin-dot i{width:6px;height:6px}.ant-spin-lg .ant-spin-dot{font-size:32px}.ant-spin-lg .ant-spin-dot i{width:14px;height:14px}.ant-spin.ant-spin-show-text .ant-spin-text{display:block}@media (-ms-high-contrast:active),(-ms-high-contrast:none){.ant-spin-blur{background:#fff;opacity:.5}}@-webkit-keyframes antSpinMove{to{opacity:1}}@keyframes antSpinMove{to{opacity:1}}@-webkit-keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}@keyframes antRotate{to{-webkit-transform:rotate(405deg);transform:rotate(405deg)}}.ant-pagination{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-pagination,.ant-pagination ol,.ant-pagination ul{margin:0;padding:0;list-style:none}.ant-pagination:after{display:block;clear:both;height:0;overflow:hidden;visibility:hidden;content:" "}.ant-pagination-item,.ant-pagination-total-text{display:inline-block;height:32px;margin-right:8px;line-height:30px;vertical-align:middle}.ant-pagination-item{min-width:32px;font-family:Arial;text-align:center;list-style:none;background-color:#fff;border:1px solid #d9d9d9;border-radius:4px;outline:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-pagination-item a{display:block;padding:0 6px;color:rgba(0,0,0,.65);-webkit-transition:none;transition:none}.ant-pagination-item a:hover{text-decoration:none}.ant-pagination-item:focus,.ant-pagination-item:hover{border-color:#1890ff;-webkit-transition:all .3s;transition:all .3s}.ant-pagination-item:focus a,.ant-pagination-item:hover a{color:#1890ff}.ant-pagination-item-active{font-weight:500;background:#fff;border-color:#1890ff}.ant-pagination-item-active a{color:#1890ff}.ant-pagination-item-active:focus,.ant-pagination-item-active:hover{border-color:#40a9ff}.ant-pagination-item-active:focus a,.ant-pagination-item-active:hover a{color:#40a9ff}.ant-pagination-jump-next,.ant-pagination-jump-prev{outline:0}.ant-pagination-jump-next .ant-pagination-item-container,.ant-pagination-jump-prev .ant-pagination-item-container{position:relative}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon{display:inline-block;font-size:12px;font-size:12px\9;-webkit-transform:scale(1) rotate(0deg);-ms-transform:scale(1) rotate(0deg);transform:scale(1) rotate(0deg);color:#1890ff;letter-spacing:-1px;opacity:0;-webkit-transition:all .2s;transition:all .2s}:root .ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon,:root .ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon{font-size:12px}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-link-icon-svg,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-link-icon-svg{top:0;right:0;bottom:0;left:0;margin:auto}.ant-pagination-jump-next .ant-pagination-item-container .ant-pagination-item-ellipsis,.ant-pagination-jump-prev .ant-pagination-item-container .ant-pagination-item-ellipsis{position:absolute;top:0;right:0;bottom:0;left:0;display:block;margin:auto;color:rgba(0,0,0,.25);letter-spacing:2px;text-align:center;text-indent:.13em;opacity:1;-webkit-transition:all .2s;transition:all .2s}.ant-pagination-jump-next:focus .ant-pagination-item-link-icon,.ant-pagination-jump-next:hover .ant-pagination-item-link-icon,.ant-pagination-jump-prev:focus .ant-pagination-item-link-icon,.ant-pagination-jump-prev:hover .ant-pagination-item-link-icon{opacity:1}.ant-pagination-jump-next:focus .ant-pagination-item-ellipsis,.ant-pagination-jump-next:hover .ant-pagination-item-ellipsis,.ant-pagination-jump-prev:focus .ant-pagination-item-ellipsis,.ant-pagination-jump-prev:hover .ant-pagination-item-ellipsis{opacity:0}.ant-pagination-jump-next,.ant-pagination-jump-prev,.ant-pagination-prev{margin-right:8px}.ant-pagination-jump-next,.ant-pagination-jump-prev,.ant-pagination-next,.ant-pagination-prev{display:inline-block;min-width:32px;height:32px;color:rgba(0,0,0,.65);font-family:Arial;line-height:32px;text-align:center;vertical-align:middle;list-style:none;border-radius:4px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-pagination-next,.ant-pagination-prev{outline:0}.ant-pagination-next a,.ant-pagination-prev a{color:rgba(0,0,0,.65);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-pagination-next:hover a,.ant-pagination-prev:hover a{border-color:#40a9ff}.ant-pagination-next .ant-pagination-item-link,.ant-pagination-prev .ant-pagination-item-link{display:block;height:100%;font-size:12px;text-align:center;background-color:#fff;border:1px solid #d9d9d9;border-radius:4px;outline:none;-webkit-transition:all .3s;transition:all .3s}.ant-pagination-next:focus .ant-pagination-item-link,.ant-pagination-next:hover .ant-pagination-item-link,.ant-pagination-prev:focus .ant-pagination-item-link,.ant-pagination-prev:hover .ant-pagination-item-link{color:#1890ff;border-color:#1890ff}.ant-pagination-disabled,.ant-pagination-disabled:focus,.ant-pagination-disabled:hover{cursor:not-allowed}.ant-pagination-disabled .ant-pagination-item-link,.ant-pagination-disabled:focus .ant-pagination-item-link,.ant-pagination-disabled:focus a,.ant-pagination-disabled:hover .ant-pagination-item-link,.ant-pagination-disabled:hover a,.ant-pagination-disabled a{color:rgba(0,0,0,.25);border-color:#d9d9d9;cursor:not-allowed}.ant-pagination-slash{margin:0 10px 0 5px}.ant-pagination-options{display:inline-block;margin-left:16px;vertical-align:middle}.ant-pagination-options-size-changer.ant-select{display:inline-block;width:auto;margin-right:8px}.ant-pagination-options-quick-jumper{display:inline-block;height:32px;line-height:32px;vertical-align:top}.ant-pagination-options-quick-jumper input{position:relative;display:inline-block;width:100%;height:32px;padding:4px 11px;color:rgba(0,0,0,.65);font-size:14px;line-height:1.5;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s;width:50px;margin:0 8px}.ant-pagination-options-quick-jumper input::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-pagination-options-quick-jumper input:-ms-input-placeholder{color:#bfbfbf}.ant-pagination-options-quick-jumper input::-webkit-input-placeholder{color:#bfbfbf}.ant-pagination-options-quick-jumper input:-moz-placeholder-shown{text-overflow:ellipsis}.ant-pagination-options-quick-jumper input:-ms-input-placeholder{text-overflow:ellipsis}.ant-pagination-options-quick-jumper input:placeholder-shown{text-overflow:ellipsis}.ant-pagination-options-quick-jumper input:focus,.ant-pagination-options-quick-jumper input:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-pagination-options-quick-jumper input:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-pagination-options-quick-jumper input-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-pagination-options-quick-jumper input-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-pagination-options-quick-jumper input[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-pagination-options-quick-jumper input[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-pagination-options-quick-jumper input{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-pagination-options-quick-jumper input-lg{height:40px;padding:6px 11px;font-size:16px}.ant-pagination-options-quick-jumper input-sm{height:24px;padding:1px 7px}.ant-pagination-simple .ant-pagination-next,.ant-pagination-simple .ant-pagination-prev{height:24px;line-height:24px;vertical-align:top}.ant-pagination-simple .ant-pagination-next .ant-pagination-item-link,.ant-pagination-simple .ant-pagination-prev .ant-pagination-item-link{height:24px;border:0}.ant-pagination-simple .ant-pagination-next .ant-pagination-item-link:after,.ant-pagination-simple .ant-pagination-prev .ant-pagination-item-link:after{height:24px;line-height:24px}.ant-pagination-simple .ant-pagination-simple-pager{display:inline-block;height:24px;margin-right:8px}.ant-pagination-simple .ant-pagination-simple-pager input{-webkit-box-sizing:border-box;box-sizing:border-box;height:100%;margin-right:8px;padding:0 6px;text-align:center;background-color:#fff;border:1px solid #d9d9d9;border-radius:4px;outline:none;-webkit-transition:border-color .3s;transition:border-color .3s}.ant-pagination-simple .ant-pagination-simple-pager input:hover{border-color:#1890ff}.ant-pagination.mini .ant-pagination-simple-pager,.ant-pagination.mini .ant-pagination-total-text{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-item{min-width:24px;height:24px;margin:0;line-height:22px}.ant-pagination.mini .ant-pagination-item:not(.ant-pagination-item-active){background:transparent;border-color:transparent}.ant-pagination.mini .ant-pagination-next,.ant-pagination.mini .ant-pagination-prev{min-width:24px;height:24px;margin:0;line-height:24px}.ant-pagination.mini .ant-pagination-next .ant-pagination-item-link,.ant-pagination.mini .ant-pagination-prev .ant-pagination-item-link{background:transparent;border-color:transparent}.ant-pagination.mini .ant-pagination-next .ant-pagination-item-link:after,.ant-pagination.mini .ant-pagination-prev .ant-pagination-item-link:after{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-jump-next,.ant-pagination.mini .ant-pagination-jump-prev{height:24px;margin-right:0;line-height:24px}.ant-pagination.mini .ant-pagination-options{margin-left:2px}.ant-pagination.mini .ant-pagination-options-quick-jumper{height:24px;line-height:24px}.ant-pagination.mini .ant-pagination-options-quick-jumper input{height:24px;padding:1px 7px;width:44px}.ant-pagination.ant-pagination-disabled{cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item{background:#f5f5f5;border-color:#d9d9d9;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item a{color:rgba(0,0,0,.25);background:transparent;border:none;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-item-active{background:#dbdbdb;border-color:transparent}.ant-pagination.ant-pagination-disabled .ant-pagination-item-active a{color:#fff}.ant-pagination.ant-pagination-disabled .ant-pagination-item-link,.ant-pagination.ant-pagination-disabled .ant-pagination-item-link:focus,.ant-pagination.ant-pagination-disabled .ant-pagination-item-link:hover{color:rgba(0,0,0,.45);background:#f5f5f5;border-color:#d9d9d9;cursor:not-allowed}.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:focus .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:hover .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:focus .ant-pagination-item-link-icon,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:hover .ant-pagination-item-link-icon{opacity:0}.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:focus .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-next:hover .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:focus .ant-pagination-item-ellipsis,.ant-pagination.ant-pagination-disabled .ant-pagination-jump-prev:hover .ant-pagination-item-ellipsis{opacity:1}@media only screen and (max-width:992px){.ant-pagination-item-after-jump-prev,.ant-pagination-item-before-jump-next{display:none}}@media only screen and (max-width:576px){.ant-pagination-options{display:none}}.ant-mention-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;font-size:14px;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;vertical-align:middle}.ant-mention-wrapper,.ant-mention-wrapper .ant-mention-editor{padding:0;color:rgba(0,0,0,.65);line-height:1.5;position:relative;width:100%}.ant-mention-wrapper .ant-mention-editor{display:inline-block;height:32px;font-size:14px;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s;display:block;height:auto;min-height:32px}.ant-mention-wrapper .ant-mention-editor::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-mention-wrapper .ant-mention-editor:-ms-input-placeholder{color:#bfbfbf}.ant-mention-wrapper .ant-mention-editor::-webkit-input-placeholder{color:#bfbfbf}.ant-mention-wrapper .ant-mention-editor:-moz-placeholder-shown{text-overflow:ellipsis}.ant-mention-wrapper .ant-mention-editor:-ms-input-placeholder{text-overflow:ellipsis}.ant-mention-wrapper .ant-mention-editor:placeholder-shown{text-overflow:ellipsis}.ant-mention-wrapper .ant-mention-editor:focus,.ant-mention-wrapper .ant-mention-editor:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-mention-wrapper .ant-mention-editor:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-mention-wrapper .ant-mention-editor-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mention-wrapper .ant-mention-editor-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mention-wrapper .ant-mention-editor[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mention-wrapper .ant-mention-editor[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-mention-wrapper .ant-mention-editor{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-mention-wrapper .ant-mention-editor-lg{height:40px;padding:6px 11px;font-size:16px}.ant-mention-wrapper .ant-mention-editor-sm{height:24px;padding:1px 7px}.ant-mention-wrapper .ant-mention-editor-wrapper{height:auto;overflow-y:auto}.ant-mention-wrapper.ant-mention-active:not(.disabled) .ant-mention-editor{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-mention-wrapper.disabled .ant-mention-editor{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mention-wrapper.disabled .ant-mention-editor:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mention-wrapper .public-DraftEditorPlaceholder-root{position:absolute;pointer-events:none}.ant-mention-wrapper .public-DraftEditorPlaceholder-root .public-DraftEditorPlaceholder-inner{height:auto;padding:5px 11px;color:#bfbfbf;white-space:pre-wrap;word-wrap:break-word;outline:none;opacity:1}.ant-mention-wrapper .DraftEditor-editorContainer .public-DraftEditor-content{height:auto;padding:5px 11px}.ant-mention-dropdown{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;top:-9999px;left:-9999px;z-index:1050;min-width:120px;max-height:250px;margin:1.5em 0 0;overflow-x:hidden;overflow-y:auto;background-color:#fff;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-mention-dropdown-placement-top{margin-top:-.1em}.ant-mention-dropdown-notfound.ant-mention-dropdown-item{color:rgba(0,0,0,.25)}.ant-mention-dropdown-notfound.ant-mention-dropdown-item .anticon-loading{display:block;color:#1890ff;text-align:center}.ant-mention-dropdown-item{position:relative;display:block;padding:5px 12px;overflow:hidden;color:rgba(0,0,0,.65);font-weight:400;line-height:22px;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.ant-mention-dropdown-item-active,.ant-mention-dropdown-item.focus,.ant-mention-dropdown-item:hover{background-color:#e6f7ff}.ant-mention-dropdown-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-mention-dropdown-item-disabled:hover{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-mention-dropdown-item-selected,.ant-mention-dropdown-item-selected:hover{color:rgba(0,0,0,.65);font-weight:700;background-color:#f5f5f5}.ant-mention-dropdown-item-divider{height:1px;margin:1px 0;overflow:hidden;line-height:0;background-color:#e8e8e8}.ant-mentions{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;font-variant:tabular-nums;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";width:100%;height:32px;color:rgba(0,0,0,.65);font-size:14px;background-color:#fff;background-image:none;border:1px solid #d9d9d9;border-radius:4px;-webkit-transition:all .3s;transition:all .3s;position:relative;display:inline-block;height:auto;padding:0;overflow:hidden;line-height:1.5;white-space:pre-wrap;vertical-align:bottom}.ant-mentions::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-mentions:-ms-input-placeholder{color:#bfbfbf}.ant-mentions::-webkit-input-placeholder{color:#bfbfbf}.ant-mentions:-moz-placeholder-shown{text-overflow:ellipsis}.ant-mentions:-ms-input-placeholder{text-overflow:ellipsis}.ant-mentions:placeholder-shown{text-overflow:ellipsis}.ant-mentions:focus,.ant-mentions:hover{border-color:#40a9ff;border-right-width:1px!important}.ant-mentions:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-mentions-disabled{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions-disabled:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mentions[disabled]{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions[disabled]:hover{border-color:#d9d9d9;border-right-width:1px!important}textarea.ant-mentions{max-width:100%;height:auto;min-height:32px;line-height:1.5;vertical-align:bottom;-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-mentions-lg{height:40px;padding:6px 11px;font-size:16px}.ant-mentions-sm{height:24px;padding:1px 7px}.ant-mentions-disabled>textarea{color:rgba(0,0,0,.25);background-color:#f5f5f5;cursor:not-allowed;opacity:1}.ant-mentions-disabled>textarea:hover{border-color:#d9d9d9;border-right-width:1px!important}.ant-mentions-focused{border-color:#40a9ff;border-right-width:1px!important;outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-mentions-measure,.ant-mentions>textarea{min-height:30px;margin:0;padding:4px 11px;overflow:inherit;overflow-x:hidden;overflow-y:auto;font-weight:inherit;font-size:inherit;font-family:inherit;font-style:inherit;-webkit-font-feature-settings:inherit;font-feature-settings:inherit;font-variant:inherit;font-size-adjust:inherit;font-stretch:inherit;line-height:inherit;direction:inherit;letter-spacing:inherit;white-space:inherit;text-align:inherit;vertical-align:top;word-wrap:break-word;word-break:inherit;-moz-tab-size:inherit;-o-tab-size:inherit;tab-size:inherit}.ant-mentions>textarea{width:100%;border:none;outline:none;resize:none}.ant-mentions>textarea::-moz-placeholder{color:#bfbfbf;opacity:1}.ant-mentions>textarea:-ms-input-placeholder{color:#bfbfbf}.ant-mentions>textarea::-webkit-input-placeholder{color:#bfbfbf}.ant-mentions>textarea:-moz-placeholder-shown{text-overflow:ellipsis}.ant-mentions>textarea:-ms-input-placeholder{text-overflow:ellipsis}.ant-mentions>textarea:placeholder-shown{text-overflow:ellipsis}.ant-mentions>textarea:-moz-read-only{cursor:default}.ant-mentions>textarea:read-only{cursor:default}.ant-mentions-measure{position:absolute;top:0;right:0;bottom:0;left:0;z-index:-1;color:transparent;pointer-events:none}.ant-mentions-measure>span{display:inline-block;min-height:1em}.ant-mentions-dropdown{margin:0;padding:0;color:rgba(0,0,0,.65);font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum",;position:absolute;top:-9999px;left:-9999px;z-index:1050;-webkit-box-sizing:border-box;box-sizing:border-box;font-size:14px;font-variant:normal;background-color:#fff;border-radius:4px;outline:none;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-mentions-dropdown-hidden{display:none}.ant-mentions-dropdown-menu{max-height:250px;margin-bottom:0;padding-left:0;overflow:auto;list-style:none;outline:none}.ant-mentions-dropdown-menu-item{position:relative;display:block;min-width:100px;padding:5px 12px;overflow:hidden;color:rgba(0,0,0,.65);font-weight:400;line-height:22px;white-space:nowrap;text-overflow:ellipsis;cursor:pointer;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-mentions-dropdown-menu-item:hover{background-color:#e6f7ff}.ant-mentions-dropdown-menu-item:first-child{border-radius:4px 4px 0 0}.ant-mentions-dropdown-menu-item:last-child{border-radius:0 0 4px 4px}.ant-mentions-dropdown-menu-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-mentions-dropdown-menu-item-disabled:hover{color:rgba(0,0,0,.25);background-color:#fff;cursor:not-allowed}.ant-mentions-dropdown-menu-item-selected{color:rgba(0,0,0,.65);font-weight:600;background-color:#fafafa}.ant-mentions-dropdown-menu-item-active{background-color:#e6f7ff}.ant-message{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:fixed;top:16px;left:0;z-index:1010;width:100%;pointer-events:none}.ant-message-notice{padding:8px;text-align:center}.ant-message-notice:first-child{margin-top:-8px}.ant-message-notice-content{display:inline-block;padding:10px 16px;background:#fff;border-radius:4px;-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15);pointer-events:all}.ant-message-success .anticon{color:#52c41a}.ant-message-error .anticon{color:#f5222d}.ant-message-warning .anticon{color:#faad14}.ant-message-info .anticon,.ant-message-loading .anticon{color:#1890ff}.ant-message .anticon{position:relative;top:1px;margin-right:8px;font-size:16px}.ant-message-notice.move-up-leave.move-up-leave-active{overflow:hidden;-webkit-animation-name:MessageMoveOut;animation-name:MessageMoveOut;-webkit-animation-duration:.3s;animation-duration:.3s}@-webkit-keyframes MessageMoveOut{0%{max-height:150px;padding:8px;opacity:1}to{max-height:0;padding:0;opacity:0}}@keyframes MessageMoveOut{0%{max-height:150px;padding:8px;opacity:1}to{max-height:0;padding:0;opacity:0}}.ant-modal{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;top:100px;width:auto;margin:0 auto;padding:0 0 24px;pointer-events:none}.ant-modal-wrap{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;overflow:auto;outline:0;-webkit-overflow-scrolling:touch}.ant-modal-title{margin:0;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:22px;word-wrap:break-word}.ant-modal-content{position:relative;background-color:#fff;background-clip:padding-box;border:0;border-radius:4px;-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15);pointer-events:auto}.ant-modal-close{position:absolute;top:0;right:0;z-index:10;padding:0;color:rgba(0,0,0,.45);font-weight:700;line-height:1;text-decoration:none;background:transparent;border:0;outline:0;cursor:pointer;-webkit-transition:color .3s;transition:color .3s}.ant-modal-close-x{display:block;width:56px;height:56px;font-size:16px;font-style:normal;line-height:56px;text-align:center;text-transform:none;text-rendering:auto}.ant-modal-close:focus,.ant-modal-close:hover{color:rgba(0,0,0,.75);text-decoration:none}.ant-modal-header{padding:16px 24px;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0}.ant-modal-body{padding:24px;font-size:14px;line-height:1.5;word-wrap:break-word}.ant-modal-footer{padding:10px 16px;text-align:right;background:transparent;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px}.ant-modal-footer button+button{margin-bottom:0;margin-left:8px}.ant-modal.zoom-appear,.ant-modal.zoom-enter{-webkit-transform:none;-ms-transform:none;transform:none;opacity:0;-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-modal-mask{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1000;height:100%;background-color:rgba(0,0,0,.45);filter:alpha(opacity=50)}.ant-modal-mask-hidden{display:none}.ant-modal-open{overflow:hidden}.ant-modal-centered{text-align:center}.ant-modal-centered:before{display:inline-block;width:0;height:100%;vertical-align:middle;content:""}.ant-modal-centered .ant-modal{top:0;display:inline-block;text-align:left;vertical-align:middle}@media (max-width:767px){.ant-modal{max-width:calc(100vw - 16px);margin:8px auto}.ant-modal-centered .ant-modal{-ms-flex:1;flex:1 1}}.ant-modal-confirm .ant-modal-close,.ant-modal-confirm .ant-modal-header{display:none}.ant-modal-confirm .ant-modal-body{padding:32px 32px 24px}.ant-modal-confirm-body-wrapper{zoom:1}.ant-modal-confirm-body-wrapper:after,.ant-modal-confirm-body-wrapper:before{display:table;content:""}.ant-modal-confirm-body-wrapper:after{clear:both}.ant-modal-confirm-body .ant-modal-confirm-title{display:block;overflow:hidden;color:rgba(0,0,0,.85);font-weight:500;font-size:16px;line-height:1.4}.ant-modal-confirm-body .ant-modal-confirm-content{margin-top:8px;color:rgba(0,0,0,.65);font-size:14px}.ant-modal-confirm-body>.anticon{float:left;margin-right:16px;font-size:22px}.ant-modal-confirm-body>.anticon+.ant-modal-confirm-title+.ant-modal-confirm-content{margin-left:38px}.ant-modal-confirm .ant-modal-confirm-btns{float:right;margin-top:24px}.ant-modal-confirm .ant-modal-confirm-btns button+button{margin-bottom:0;margin-left:8px}.ant-modal-confirm-error .ant-modal-confirm-body>.anticon{color:#f5222d}.ant-modal-confirm-confirm .ant-modal-confirm-body>.anticon,.ant-modal-confirm-warning .ant-modal-confirm-body>.anticon{color:#faad14}.ant-modal-confirm-info .ant-modal-confirm-body>.anticon{color:#1890ff}.ant-modal-confirm-success .ant-modal-confirm-body>.anticon{color:#52c41a}.ant-notification{-webkit-box-sizing:border-box;box-sizing:border-box;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:fixed;z-index:1010;width:384px;max-width:calc(100vw - 32px);margin:0 24px 0 0}.ant-notification-bottomLeft,.ant-notification-topLeft{margin-right:0;margin-left:24px}.ant-notification-bottomLeft .ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-bottomLeft .ant-notification-fade-enter.ant-notification-fade-enter-active,.ant-notification-topLeft .ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-topLeft .ant-notification-fade-enter.ant-notification-fade-enter-active{-webkit-animation-name:NotificationLeftFadeIn;animation-name:NotificationLeftFadeIn}.ant-notification-close-icon{font-size:14px;cursor:pointer}.ant-notification-notice{position:relative;margin-bottom:16px;padding:16px 24px;overflow:hidden;line-height:1.5;background:#fff;border-radius:4px;-webkit-box-shadow:0 4px 12px rgba(0,0,0,.15);box-shadow:0 4px 12px rgba(0,0,0,.15)}.ant-notification-notice-message{display:inline-block;margin-bottom:8px;color:rgba(0,0,0,.85);font-size:16px;line-height:24px}.ant-notification-notice-message-single-line-auto-margin{display:block;width:calc(264px - 100%);max-width:4px;background-color:transparent;pointer-events:none}.ant-notification-notice-message-single-line-auto-margin:before{display:block;content:""}.ant-notification-notice-description{font-size:14px}.ant-notification-notice-closable .ant-notification-notice-message{padding-right:24px}.ant-notification-notice-with-icon .ant-notification-notice-message{margin-bottom:4px;margin-left:48px;font-size:16px}.ant-notification-notice-with-icon .ant-notification-notice-description{margin-left:48px;font-size:14px}.ant-notification-notice-icon{position:absolute;margin-left:4px;font-size:24px;line-height:24px}.anticon.ant-notification-notice-icon-success{color:#52c41a}.anticon.ant-notification-notice-icon-info{color:#1890ff}.anticon.ant-notification-notice-icon-warning{color:#faad14}.anticon.ant-notification-notice-icon-error{color:#f5222d}.ant-notification-notice-close{position:absolute;top:16px;right:22px;color:rgba(0,0,0,.45);outline:none}.ant-notification-notice-close:hover{color:rgba(0,0,0,.67)}.ant-notification-notice-btn{float:right;margin-top:16px}.ant-notification .notification-fade-effect{-webkit-animation-duration:.24s;animation-duration:.24s;-webkit-animation-timing-function:cubic-bezier(.645,.045,.355,1);animation-timing-function:cubic-bezier(.645,.045,.355,1);-webkit-animation-fill-mode:both;animation-fill-mode:both}.ant-notification-fade-appear,.ant-notification-fade-enter{opacity:0;-webkit-animation-play-state:paused;animation-play-state:paused}.ant-notification-fade-appear,.ant-notification-fade-enter,.ant-notification-fade-leave{-webkit-animation-duration:.24s;animation-duration:.24s;-webkit-animation-timing-function:cubic-bezier(.645,.045,.355,1);animation-timing-function:cubic-bezier(.645,.045,.355,1);-webkit-animation-fill-mode:both;animation-fill-mode:both}.ant-notification-fade-leave{-webkit-animation-duration:.2s;animation-duration:.2s;-webkit-animation-play-state:paused;animation-play-state:paused}.ant-notification-fade-appear.ant-notification-fade-appear-active,.ant-notification-fade-enter.ant-notification-fade-enter-active{-webkit-animation-name:NotificationFadeIn;animation-name:NotificationFadeIn;-webkit-animation-play-state:running;animation-play-state:running}.ant-notification-fade-leave.ant-notification-fade-leave-active{-webkit-animation-name:NotificationFadeOut;animation-name:NotificationFadeOut;-webkit-animation-play-state:running;animation-play-state:running}@-webkit-keyframes NotificationFadeIn{0%{left:384px;opacity:0}to{left:0;opacity:1}}@keyframes NotificationFadeIn{0%{left:384px;opacity:0}to{left:0;opacity:1}}@-webkit-keyframes NotificationLeftFadeIn{0%{right:384px;opacity:0}to{right:0;opacity:1}}@keyframes NotificationLeftFadeIn{0%{right:384px;opacity:0}to{right:0;opacity:1}}@-webkit-keyframes NotificationFadeOut{0%{max-height:150px;margin-bottom:16px;padding-top:16px 24px;padding-bottom:16px 24px;opacity:1}to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}}@keyframes NotificationFadeOut{0%{max-height:150px;margin-bottom:16px;padding-top:16px 24px;padding-bottom:16px 24px;opacity:1}to{max-height:0;margin-bottom:0;padding-top:0;padding-bottom:0;opacity:0}}.ant-page-header{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;padding:16px 24px;background-color:#fff}.ant-page-header-ghost{background-color:inherit}.ant-page-header.has-breadcrumb{padding-top:12px}.ant-page-header.has-footer{padding-bottom:0}.ant-page-header-back{float:left;margin:8px 16px 8px 0;font-size:16px;line-height:1}.ant-page-header-back-button{color:#1890ff;text-decoration:none;outline:none;-webkit-transition:color .3s;transition:color .3s;color:#000;cursor:pointer}.ant-page-header-back-button:focus,.ant-page-header-back-button:hover{color:#40a9ff}.ant-page-header-back-button:active{color:#096dd9}.ant-page-header .ant-divider-vertical{height:14px;margin:0 12px;vertical-align:middle}.ant-breadcrumb+.ant-page-header-heading{margin-top:8px}.ant-page-header-heading{width:100%;overflow:hidden}.ant-page-header-heading-title{display:block;float:left;margin-bottom:0;padding-right:12px;color:rgba(0,0,0,.85);font-weight:600;font-size:20px;line-height:32px}.ant-page-header-heading .ant-avatar{float:left;margin-right:12px}.ant-page-header-heading-sub-title{float:left;margin:5px 12px 5px 0;color:rgba(0,0,0,.45);font-size:14px;line-height:22px}.ant-page-header-heading-tags{float:left;margin:4px 0}.ant-page-header-heading-extra{float:right}.ant-page-header-heading-extra>*{margin-left:8px}.ant-page-header-heading-extra>:first-child{margin-left:0}.ant-page-header-content{padding-top:12px;overflow:hidden}.ant-page-header-footer{margin-top:16px}.ant-page-header-footer .ant-tabs-bar{margin-bottom:1px;border-bottom:0}.ant-page-header-footer .ant-tabs-bar .ant-tabs-nav .ant-tabs-tab{padding:8px;font-size:16px}@media (max-width:576px){.ant-page-header-heading-extra{display:block;float:unset;width:100%;padding-top:12px;overflow:hidden}}.ant-popover{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:absolute;top:0;left:0;z-index:1030;font-weight:400;white-space:normal;text-align:left;cursor:auto;-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text}.ant-popover:after{position:absolute;background:hsla(0,0%,100%,.01);content:""}.ant-popover-hidden{display:none}.ant-popover-placement-top,.ant-popover-placement-topLeft,.ant-popover-placement-topRight{padding-bottom:10px}.ant-popover-placement-right,.ant-popover-placement-rightBottom,.ant-popover-placement-rightTop{padding-left:10px}.ant-popover-placement-bottom,.ant-popover-placement-bottomLeft,.ant-popover-placement-bottomRight{padding-top:10px}.ant-popover-placement-left,.ant-popover-placement-leftBottom,.ant-popover-placement-leftTop{padding-right:10px}.ant-popover-inner{background-color:#fff;background-clip:padding-box;border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15);-webkit-box-shadow:0 0 8px rgba(0,0,0,.15)\9;box-shadow:0 0 8px rgba(0,0,0,.15)\9}@media (-ms-high-contrast:none),screen and (-ms-high-contrast:active){.ant-popover-inner{-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}}.ant-popover-title{min-width:177px;min-height:32px;margin:0;padding:5px 16px 4px;color:rgba(0,0,0,.85);font-weight:500;border-bottom:1px solid #e8e8e8}.ant-popover-inner-content{padding:12px 16px;color:rgba(0,0,0,.65)}.ant-popover-message{position:relative;padding:4px 0 12px;color:rgba(0,0,0,.65);font-size:14px}.ant-popover-message>.anticon{position:absolute;top:8px;color:#faad14;font-size:14px}.ant-popover-message-title{padding-left:22px}.ant-popover-buttons{margin-bottom:4px;text-align:right}.ant-popover-buttons button{margin-left:8px}.ant-popover-arrow{position:absolute;display:block;width:8.48528137px;height:8.48528137px;background:transparent;border-style:solid;border-width:4.24264069px;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg)}.ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-topLeft>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-topRight>.ant-popover-content>.ant-popover-arrow{bottom:6.2px;border-color:transparent #fff #fff transparent;-webkit-box-shadow:3px 3px 7px rgba(0,0,0,.07);box-shadow:3px 3px 7px rgba(0,0,0,.07)}.ant-popover-placement-top>.ant-popover-content>.ant-popover-arrow{left:50%;-webkit-transform:translateX(-50%) rotate(45deg);-ms-transform:translateX(-50%) rotate(45deg);transform:translateX(-50%) rotate(45deg)}.ant-popover-placement-topLeft>.ant-popover-content>.ant-popover-arrow{left:16px}.ant-popover-placement-topRight>.ant-popover-content>.ant-popover-arrow{right:16px}.ant-popover-placement-right>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-rightBottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-rightTop>.ant-popover-content>.ant-popover-arrow{left:6px;border-color:transparent transparent #fff #fff;-webkit-box-shadow:-3px 3px 7px rgba(0,0,0,.07);box-shadow:-3px 3px 7px rgba(0,0,0,.07)}.ant-popover-placement-right>.ant-popover-content>.ant-popover-arrow{top:50%;-webkit-transform:translateY(-50%) rotate(45deg);-ms-transform:translateY(-50%) rotate(45deg);transform:translateY(-50%) rotate(45deg)}.ant-popover-placement-rightTop>.ant-popover-content>.ant-popover-arrow{top:12px}.ant-popover-placement-rightBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}.ant-popover-placement-bottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-bottomLeft>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-bottomRight>.ant-popover-content>.ant-popover-arrow{top:6px;border-color:#fff transparent transparent #fff;-webkit-box-shadow:-2px -2px 5px rgba(0,0,0,.06);box-shadow:-2px -2px 5px rgba(0,0,0,.06)}.ant-popover-placement-bottom>.ant-popover-content>.ant-popover-arrow{left:50%;-webkit-transform:translateX(-50%) rotate(45deg);-ms-transform:translateX(-50%) rotate(45deg);transform:translateX(-50%) rotate(45deg)}.ant-popover-placement-bottomLeft>.ant-popover-content>.ant-popover-arrow{left:16px}.ant-popover-placement-bottomRight>.ant-popover-content>.ant-popover-arrow{right:16px}.ant-popover-placement-left>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow,.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{right:6px;border-color:#fff #fff transparent transparent;-webkit-box-shadow:3px -3px 7px rgba(0,0,0,.07);box-shadow:3px -3px 7px rgba(0,0,0,.07)}.ant-popover-placement-left>.ant-popover-content>.ant-popover-arrow{top:50%;-webkit-transform:translateY(-50%) rotate(45deg);-ms-transform:translateY(-50%) rotate(45deg);transform:translateY(-50%) rotate(45deg)}.ant-popover-placement-leftTop>.ant-popover-content>.ant-popover-arrow{top:12px}.ant-popover-placement-leftBottom>.ant-popover-content>.ant-popover-arrow{bottom:12px}.ant-progress{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block}.ant-progress-line{position:relative;width:100%;font-size:14px}.ant-progress-small.ant-progress-line,.ant-progress-small.ant-progress-line .ant-progress-text .anticon{font-size:12px}.ant-progress-outer{display:inline-block;width:100%;margin-right:0;padding-right:0}.ant-progress-show-info .ant-progress-outer{margin-right:calc(-2em - 8px);padding-right:calc(2em + 8px)}.ant-progress-inner{position:relative;display:inline-block;width:100%;overflow:hidden;vertical-align:middle;background-color:#f5f5f5;border-radius:100px}.ant-progress-circle-trail{stroke:#f5f5f5}.ant-progress-circle-path{-webkit-animation:ant-progress-appear .3s;animation:ant-progress-appear .3s}.ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#1890ff}.ant-progress-bg,.ant-progress-success-bg{position:relative;background-color:#1890ff;border-radius:100px;-webkit-transition:all .4s cubic-bezier(.08,.82,.17,1) 0s;transition:all .4s cubic-bezier(.08,.82,.17,1) 0s}.ant-progress-success-bg{position:absolute;top:0;left:0;background-color:#52c41a}.ant-progress-text{display:inline-block;width:2em;margin-left:8px;color:rgba(0,0,0,.45);font-size:1em;line-height:1;white-space:nowrap;text-align:left;vertical-align:middle;word-break:normal}.ant-progress-text .anticon{font-size:14px}.ant-progress-status-active .ant-progress-bg:before{position:absolute;top:0;right:0;bottom:0;left:0;background:#fff;border-radius:10px;opacity:0;-webkit-animation:ant-progress-active 2.4s cubic-bezier(.23,1,.32,1) infinite;animation:ant-progress-active 2.4s cubic-bezier(.23,1,.32,1) infinite;content:""}.ant-progress-status-exception .ant-progress-bg{background-color:#f5222d}.ant-progress-status-exception .ant-progress-text{color:#f5222d}.ant-progress-status-exception .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#f5222d}.ant-progress-status-success .ant-progress-bg{background-color:#52c41a}.ant-progress-status-success .ant-progress-text{color:#52c41a}.ant-progress-status-success .ant-progress-inner:not(.ant-progress-circle-gradient) .ant-progress-circle-path{stroke:#52c41a}.ant-progress-circle .ant-progress-inner{position:relative;line-height:1;background-color:transparent}.ant-progress-circle .ant-progress-text{position:absolute;top:50%;left:50%;width:100%;margin:0;padding:0;color:rgba(0,0,0,.65);line-height:1;white-space:normal;text-align:center;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ant-progress-circle .ant-progress-text .anticon{font-size:1.16666667em}.ant-progress-circle.ant-progress-status-exception .ant-progress-text{color:#f5222d}.ant-progress-circle.ant-progress-status-success .ant-progress-text{color:#52c41a}@-webkit-keyframes ant-progress-active{0%{width:0;opacity:.1}20%{width:0;opacity:.5}to{width:100%;opacity:0}}@keyframes ant-progress-active{0%{width:0;opacity:.1}20%{width:0;opacity:.5}to{width:100%;opacity:0}}.ant-rate{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;margin:0;padding:0;color:#fadb14;font-size:20px;line-height:unset;list-style:none;outline:none}.ant-rate-disabled .ant-rate-star{cursor:default}.ant-rate-disabled .ant-rate-star:hover{-webkit-transform:scale(1);-ms-transform:scale(1);transform:scale(1)}.ant-rate-star{position:relative;display:inline-block;margin:0;padding:0;color:inherit;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-rate-star:not(:last-child){margin-right:8px}.ant-rate-star>div:focus{outline:0}.ant-rate-star>div:focus,.ant-rate-star>div:hover{-webkit-transform:scale(1.1);-ms-transform:scale(1.1);transform:scale(1.1)}.ant-rate-star-first,.ant-rate-star-second{color:#e8e8e8;-webkit-transition:all .3s;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-rate-star-first .anticon,.ant-rate-star-second .anticon{vertical-align:middle}.ant-rate-star-first{position:absolute;top:0;left:0;width:50%;height:100%;overflow:hidden;opacity:0}.ant-rate-star-half .ant-rate-star-first,.ant-rate-star-half .ant-rate-star-second{opacity:1}.ant-rate-star-full .ant-rate-star-second,.ant-rate-star-half .ant-rate-star-first{color:inherit}.ant-rate-text{display:inline-block;margin-left:8px;font-size:14px}.ant-result{padding:48px 32px}.ant-result-success .ant-result-icon>.anticon{color:#52c41a}.ant-result-error .ant-result-icon>.anticon{color:#f5222d}.ant-result-info .ant-result-icon>.anticon{color:#1890ff}.ant-result-warning .ant-result-icon>.anticon{color:#faad14}.ant-result-image{width:250px;height:295px;margin:auto}.ant-result-icon{margin-bottom:24px;text-align:center}.ant-result-icon>.anticon{font-size:72px}.ant-result-title{color:rgba(0,0,0,.85);font-size:24px;line-height:1.8;text-align:center}.ant-result-subtitle{color:rgba(0,0,0,.45);font-size:14px;line-height:1.6;text-align:center}.ant-result-extra{margin-top:32px;text-align:center}.ant-result-extra>*{margin-right:8px}.ant-result-extra>:last-child{margin-right:0}.ant-result-content{margin-top:24px;padding:24px 40px;background-color:#fafafa}.ant-skeleton{display:table;width:100%}.ant-skeleton-header{display:table-cell;padding-right:16px;vertical-align:top}.ant-skeleton-header .ant-skeleton-avatar{display:inline-block;vertical-align:top;background:#f2f2f2;width:32px;height:32px;line-height:32px}.ant-skeleton-header .ant-skeleton-avatar.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-header .ant-skeleton-avatar-lg{width:40px;height:40px;line-height:40px}.ant-skeleton-header .ant-skeleton-avatar-lg.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-header .ant-skeleton-avatar-sm{width:24px;height:24px;line-height:24px}.ant-skeleton-header .ant-skeleton-avatar-sm.ant-skeleton-avatar-circle{border-radius:50%}.ant-skeleton-content{display:table-cell;width:100%;vertical-align:top}.ant-skeleton-content .ant-skeleton-title{width:100%;height:16px;margin-top:16px;background:#f2f2f2}.ant-skeleton-content .ant-skeleton-title+.ant-skeleton-paragraph{margin-top:24px}.ant-skeleton-content .ant-skeleton-paragraph{padding:0}.ant-skeleton-content .ant-skeleton-paragraph>li{width:100%;height:16px;list-style:none;background:#f2f2f2}.ant-skeleton-content .ant-skeleton-paragraph>li:last-child:not(:first-child):not(:nth-child(2)){width:61%}.ant-skeleton-content .ant-skeleton-paragraph>li+li{margin-top:16px}.ant-skeleton-with-avatar .ant-skeleton-content .ant-skeleton-title{margin-top:12px}.ant-skeleton-with-avatar .ant-skeleton-content .ant-skeleton-title+.ant-skeleton-paragraph{margin-top:28px}.ant-skeleton.ant-skeleton-active .ant-skeleton-avatar,.ant-skeleton.ant-skeleton-active .ant-skeleton-content .ant-skeleton-paragraph>li,.ant-skeleton.ant-skeleton-active .ant-skeleton-content .ant-skeleton-title{background:-webkit-gradient(linear,left top,right top,color-stop(25%,#f2f2f2),color-stop(37%,#e6e6e6),color-stop(63%,#f2f2f2));background:linear-gradient(90deg,#f2f2f2 25%,#e6e6e6 37%,#f2f2f2 63%);background-size:400% 100%;-webkit-animation:ant-skeleton-loading 1.4s ease infinite;animation:ant-skeleton-loading 1.4s ease infinite}@-webkit-keyframes ant-skeleton-loading{0%{background-position:100% 50%}to{background-position:0 50%}}@keyframes ant-skeleton-loading{0%{background-position:100% 50%}to{background-position:0 50%}}.ant-slider{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;height:12px;margin:14px 6px 10px;padding:4px 0;cursor:pointer;-ms-touch-action:none;touch-action:none}.ant-slider-vertical{width:12px;height:100%;margin:6px 10px;padding:0 4px}.ant-slider-vertical .ant-slider-rail{width:4px;height:100%}.ant-slider-vertical .ant-slider-track{width:4px}.ant-slider-vertical .ant-slider-handle{margin-bottom:-7px;margin-left:-5px}.ant-slider-vertical .ant-slider-mark{top:0;left:12px;width:18px;height:100%}.ant-slider-vertical .ant-slider-mark-text{left:4px;white-space:nowrap}.ant-slider-vertical .ant-slider-step{width:4px;height:100%}.ant-slider-vertical .ant-slider-dot{top:auto;left:2px;margin-bottom:-4px}.ant-slider-tooltip .ant-tooltip-inner{min-width:unset}.ant-slider-with-marks{margin-bottom:28px}.ant-slider-rail{width:100%;background-color:#f5f5f5;border-radius:2px}.ant-slider-rail,.ant-slider-track{position:absolute;height:4px;-webkit-transition:background-color .3s;transition:background-color .3s}.ant-slider-track{background-color:#91d5ff;border-radius:4px}.ant-slider-handle{position:absolute;width:14px;height:14px;margin-top:-5px;background-color:#fff;border:2px solid #91d5ff;border-radius:50%;-webkit-box-shadow:0;box-shadow:0;cursor:pointer;-webkit-transition:border-color .3s,-webkit-box-shadow .6s,-webkit-transform .3s cubic-bezier(.18,.89,.32,1.28);transition:border-color .3s,-webkit-box-shadow .6s,-webkit-transform .3s cubic-bezier(.18,.89,.32,1.28);transition:border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28);transition:border-color .3s,box-shadow .6s,transform .3s cubic-bezier(.18,.89,.32,1.28),-webkit-box-shadow .6s,-webkit-transform .3s cubic-bezier(.18,.89,.32,1.28)}.ant-slider-handle:focus{border-color:#46a6ff;outline:none;-webkit-box-shadow:0 0 0 5px rgba(24,144,255,.2);box-shadow:0 0 0 5px rgba(24,144,255,.2)}.ant-slider-handle.ant-tooltip-open{border-color:#1890ff}.ant-slider:hover .ant-slider-rail{background-color:#e1e1e1}.ant-slider:hover .ant-slider-track{background-color:#69c0ff}.ant-slider:hover .ant-slider-handle:not(.ant-tooltip-open){border-color:#69c0ff}.ant-slider-mark{position:absolute;top:14px;left:0;width:100%;font-size:14px}.ant-slider-mark-text{position:absolute;display:inline-block;color:rgba(0,0,0,.45);text-align:center;word-break:keep-all;cursor:pointer}.ant-slider-mark-text-active{color:rgba(0,0,0,.65)}.ant-slider-step{position:absolute;width:100%;height:4px;background:transparent}.ant-slider-dot{position:absolute;top:-2px;width:8px;height:8px;background-color:#fff;border:2px solid #e8e8e8;border-radius:50%;cursor:pointer}.ant-slider-dot,.ant-slider-dot:first-child,.ant-slider-dot:last-child{margin-left:-4px}.ant-slider-dot-active{border-color:#8cc8ff}.ant-slider-disabled{cursor:not-allowed}.ant-slider-disabled .ant-slider-track{background-color:rgba(0,0,0,.25)!important}.ant-slider-disabled .ant-slider-dot,.ant-slider-disabled .ant-slider-handle{background-color:#fff;border-color:rgba(0,0,0,.25)!important;-webkit-box-shadow:none;box-shadow:none;cursor:not-allowed}.ant-slider-disabled .ant-slider-dot,.ant-slider-disabled .ant-slider-mark-text{cursor:not-allowed!important}.ant-statistic{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-statistic-title{margin-bottom:4px;color:rgba(0,0,0,.45);font-size:14px}.ant-statistic-content{color:rgba(0,0,0,.85);font-size:24px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"}.ant-statistic-content-value-decimal{font-size:16px}.ant-statistic-content-prefix,.ant-statistic-content-suffix{display:inline-block}.ant-statistic-content-prefix{margin-right:4px}.ant-statistic-content-suffix{margin-left:4px;font-size:16px}.ant-steps{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:-ms-flexbox;display:flex;width:100%;font-size:0}.ant-steps-item{position:relative;display:inline-block;-ms-flex:1;flex:1 1;overflow:hidden;vertical-align:top}.ant-steps-item-container{outline:none}.ant-steps-item:last-child{-ms-flex:none;flex:none}.ant-steps-item:last-child>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after,.ant-steps-item:last-child>.ant-steps-item-container>.ant-steps-item-tail{display:none}.ant-steps-item-content,.ant-steps-item-icon{display:inline-block;vertical-align:top}.ant-steps-item-icon{width:32px;height:32px;margin-right:8px;font-size:16px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei","Helvetica Neue",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";line-height:32px;text-align:center;border:1px solid rgba(0,0,0,.25);border-radius:32px;-webkit-transition:background-color .3s,border-color .3s;transition:background-color .3s,border-color .3s}.ant-steps-item-icon>.ant-steps-icon{position:relative;top:-1px;color:#1890ff;line-height:1}.ant-steps-item-tail{position:absolute;top:12px;left:0;width:100%;padding:0 10px}.ant-steps-item-tail:after{display:inline-block;width:100%;height:1px;background:#e8e8e8;border-radius:1px;-webkit-transition:background .3s;transition:background .3s;content:""}.ant-steps-item-title{position:relative;display:inline-block;padding-right:16px;color:rgba(0,0,0,.65);font-size:16px;line-height:32px}.ant-steps-item-title:after{position:absolute;top:16px;left:100%;display:block;width:9999px;height:1px;background:#e8e8e8;content:""}.ant-steps-item-subtitle{display:inline;margin-left:8px;font-weight:400}.ant-steps-item-description,.ant-steps-item-subtitle{color:rgba(0,0,0,.45);font-size:14px}.ant-steps-item-wait .ant-steps-item-icon{background-color:#fff;border-color:rgba(0,0,0,.25)}.ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon{color:rgba(0,0,0,.25)}.ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:rgba(0,0,0,.25)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.45)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.45)}.ant-steps-item-wait>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item-process .ant-steps-item-icon{background-color:#fff;border-color:#1890ff}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#1890ff}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#1890ff}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.85)}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.65)}.ant-steps-item-process>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item-process .ant-steps-item-icon{background:#1890ff}.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#fff}.ant-steps-item-process .ant-steps-item-title{font-weight:500}.ant-steps-item-finish .ant-steps-item-icon{background-color:#fff;border-color:#1890ff}.ant-steps-item-finish .ant-steps-item-icon>.ant-steps-icon{color:#1890ff}.ant-steps-item-finish .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#1890ff}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:rgba(0,0,0,.65)}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#1890ff}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:rgba(0,0,0,.45)}.ant-steps-item-finish>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#1890ff}.ant-steps-item-error .ant-steps-item-icon{background-color:#fff;border-color:#f5222d}.ant-steps-item-error .ant-steps-item-icon>.ant-steps-icon{color:#f5222d}.ant-steps-item-error .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title{color:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{background-color:#e8e8e8}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-description{color:#f5222d}.ant-steps-item-error>.ant-steps-item-container>.ant-steps-item-tail:after{background-color:#e8e8e8}.ant-steps-item.ant-steps-next-error .ant-steps-item-title:after{background:#f5222d}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]{cursor:pointer}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-description,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-icon .ant-steps-icon,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button] .ant-steps-item-title{-webkit-transition:color .3s;transition:color .3s}.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-description,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-subtitle,.ant-steps .ant-steps-item:not(.ant-steps-item-active)>.ant-steps-item-container[role=button]:hover .ant-steps-item-title{color:#1890ff}.ant-steps .ant-steps-item:not(.ant-steps-item-active):not(.ant-steps-item-process)>.ant-steps-item-container[role=button]:hover .ant-steps-item-icon{border-color:#1890ff}.ant-steps .ant-steps-item:not(.ant-steps-item-active):not(.ant-steps-item-process)>.ant-steps-item-container[role=button]:hover .ant-steps-item-icon .ant-steps-icon{color:#1890ff}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item{margin-right:16px;white-space:nowrap}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child{margin-right:0}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child .ant-steps-item-title{padding-right:0}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item-tail{display:none}.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item-description{max-width:140px;white-space:normal}.ant-steps-item-custom .ant-steps-item-icon{height:auto;background:none;border:0}.ant-steps-item-custom .ant-steps-item-icon>.ant-steps-icon{top:0;left:.5px;width:32px;height:32px;font-size:24px;line-height:32px}.ant-steps-item-custom.ant-steps-item-process .ant-steps-item-icon>.ant-steps-icon{color:#1890ff}.ant-steps:not(.ant-steps-vertical) .ant-steps-item-custom .ant-steps-item-icon{width:auto}.ant-steps-small.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item{margin-right:12px}.ant-steps-small.ant-steps-horizontal:not(.ant-steps-label-vertical) .ant-steps-item:last-child{margin-right:0}.ant-steps-small .ant-steps-item-icon{width:24px;height:24px;font-size:12px;line-height:24px;text-align:center;border-radius:24px}.ant-steps-small .ant-steps-item-title{padding-right:12px;font-size:14px;line-height:24px}.ant-steps-small .ant-steps-item-title:after{top:12px}.ant-steps-small .ant-steps-item-description{color:rgba(0,0,0,.45);font-size:14px}.ant-steps-small .ant-steps-item-tail{top:8px}.ant-steps-small .ant-steps-item-custom .ant-steps-item-icon{width:inherit;height:inherit;line-height:inherit;background:none;border:0;border-radius:0}.ant-steps-small .ant-steps-item-custom .ant-steps-item-icon>.ant-steps-icon{font-size:24px;line-height:24px;-webkit-transform:none;-ms-transform:none;transform:none}.ant-steps-vertical{display:block}.ant-steps-vertical .ant-steps-item{display:block;overflow:visible}.ant-steps-vertical .ant-steps-item-icon{float:left;margin-right:16px}.ant-steps-vertical .ant-steps-item-content{display:block;min-height:48px;overflow:hidden}.ant-steps-vertical .ant-steps-item-title{line-height:32px}.ant-steps-vertical .ant-steps-item-description{padding-bottom:12px}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{position:absolute;top:0;left:16px;width:1px;height:100%;padding:38px 0 6px}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail:after{width:1px;height:100%}.ant-steps-vertical>.ant-steps-item:not(:last-child)>.ant-steps-item-container>.ant-steps-item-tail{display:block}.ant-steps-vertical>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{display:none}.ant-steps-vertical.ant-steps-small .ant-steps-item-container .ant-steps-item-tail{position:absolute;top:0;left:12px;padding:30px 0 6px}.ant-steps-vertical.ant-steps-small .ant-steps-item-container .ant-steps-item-title{line-height:24px}@media (max-width:480px){.ant-steps-horizontal.ant-steps-label-horizontal{display:block}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item{display:block;overflow:visible}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-icon{float:left;margin-right:16px}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-content{display:block;min-height:48px;overflow:hidden}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-title{line-height:32px}.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item-description{padding-bottom:12px}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{position:absolute;top:0;left:16px;width:1px;height:100%;padding:38px 0 6px}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail:after{width:1px;height:100%}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item:not(:last-child)>.ant-steps-item-container>.ant-steps-item-tail{display:block}.ant-steps-horizontal.ant-steps-label-horizontal>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-content>.ant-steps-item-title:after{display:none}.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item-container .ant-steps-item-tail{position:absolute;top:0;left:12px;padding:30px 0 6px}.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item-container .ant-steps-item-title{line-height:24px}}.ant-steps-label-vertical .ant-steps-item{overflow:visible}.ant-steps-label-vertical .ant-steps-item-tail{margin-left:58px;padding:3.5px 24px}.ant-steps-label-vertical .ant-steps-item-content{display:block;width:116px;margin-top:8px;text-align:center}.ant-steps-label-vertical .ant-steps-item-icon{display:inline-block;margin-left:42px}.ant-steps-label-vertical .ant-steps-item-title{padding-right:0}.ant-steps-label-vertical .ant-steps-item-title:after{display:none}.ant-steps-label-vertical .ant-steps-item-subtitle{display:block;margin-bottom:4px;margin-left:0;line-height:1.5}.ant-steps-label-vertical.ant-steps-small:not(.ant-steps-dot) .ant-steps-item-icon{margin-left:46px}.ant-steps-dot .ant-steps-item-title,.ant-steps-dot.ant-steps-small .ant-steps-item-title{line-height:1.5}.ant-steps-dot .ant-steps-item-tail,.ant-steps-dot.ant-steps-small .ant-steps-item-tail{top:2px;width:100%;margin:0 0 0 70px;padding:0}.ant-steps-dot .ant-steps-item-tail:after,.ant-steps-dot.ant-steps-small .ant-steps-item-tail:after{width:calc(100% - 20px);height:3px;margin-left:12px}.ant-steps-dot .ant-steps-item:first-child .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item:first-child .ant-steps-icon-dot{left:2px}.ant-steps-dot .ant-steps-item-icon,.ant-steps-dot.ant-steps-small .ant-steps-item-icon{width:8px;height:8px;margin-left:67px;padding-right:0;line-height:8px;background:transparent;border:0}.ant-steps-dot .ant-steps-item-icon .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item-icon .ant-steps-icon-dot{position:relative;float:left;width:100%;height:100%;border-radius:100px;-webkit-transition:all .3s;transition:all .3s}.ant-steps-dot .ant-steps-item-icon .ant-steps-icon-dot:after,.ant-steps-dot.ant-steps-small .ant-steps-item-icon .ant-steps-icon-dot:after{position:absolute;top:-12px;left:-26px;width:60px;height:32px;background:rgba(0,0,0,.001);content:""}.ant-steps-dot .ant-steps-item-content,.ant-steps-dot.ant-steps-small .ant-steps-item-content{width:140px}.ant-steps-dot .ant-steps-item-process .ant-steps-item-icon,.ant-steps-dot.ant-steps-small .ant-steps-item-process .ant-steps-item-icon{width:10px;height:10px;line-height:10px}.ant-steps-dot .ant-steps-item-process .ant-steps-item-icon .ant-steps-icon-dot,.ant-steps-dot.ant-steps-small .ant-steps-item-process .ant-steps-item-icon .ant-steps-icon-dot{top:-1px}.ant-steps-vertical.ant-steps-dot .ant-steps-item-icon{margin-top:8px;margin-left:0}.ant-steps-vertical.ant-steps-dot .ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{top:2px;left:-9px;margin:0;padding:22px 0 4px}.ant-steps-vertical.ant-steps-dot .ant-steps-item:first-child .ant-steps-icon-dot{left:0}.ant-steps-vertical.ant-steps-dot .ant-steps-item-process .ant-steps-icon-dot{left:-2px}.ant-steps-navigation{padding-top:12px}.ant-steps-navigation.ant-steps-small .ant-steps-item-container{margin-left:-12px}.ant-steps-navigation .ant-steps-item{overflow:visible;text-align:center}.ant-steps-navigation .ant-steps-item-container{display:inline-block;height:100%;margin-left:-16px;padding-bottom:12px;text-align:left;-webkit-transition:opacity .3s;transition:opacity .3s}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-content{max-width:auto}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-title{max-width:100%;padding-right:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-steps-navigation .ant-steps-item-container .ant-steps-item-title:after{display:none}.ant-steps-navigation .ant-steps-item:not(.ant-steps-item-active) .ant-steps-item-container[role=button]{cursor:pointer}.ant-steps-navigation .ant-steps-item:not(.ant-steps-item-active) .ant-steps-item-container[role=button]:hover{opacity:.85}.ant-steps-navigation .ant-steps-item:last-child{-ms-flex:1;flex:1 1}.ant-steps-navigation .ant-steps-item:last-child:after{display:none}.ant-steps-navigation .ant-steps-item:after{position:absolute;top:50%;left:100%;display:inline-block;width:12px;height:12px;margin-top:-14px;margin-left:-2px;border:1px solid rgba(0,0,0,.25);border-bottom:none;border-left:none;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);transform:rotate(45deg);content:""}.ant-steps-navigation .ant-steps-item:before{position:absolute;bottom:0;left:50%;display:inline-block;width:0;height:3px;background-color:#1890ff;-webkit-transition:width .3s,left .3s;transition:width .3s,left .3s;-webkit-transition-timing-function:ease-out;transition-timing-function:ease-out;content:""}.ant-steps-navigation .ant-steps-item.ant-steps-item-active:before{left:0;width:100%}@media (max-width:480px){.ant-steps-navigation>.ant-steps-item{margin-right:0!important}.ant-steps-navigation>.ant-steps-item:before{display:none}.ant-steps-navigation>.ant-steps-item.ant-steps-item-active:before{top:0;right:0;left:unset;display:block;width:3px;height:calc(100% - 24px)}.ant-steps-navigation>.ant-steps-item:after{position:relative;top:-2px;left:50%;display:block;width:8px;height:8px;margin-bottom:8px;text-align:center;-webkit-transform:rotate(135deg);-ms-transform:rotate(135deg);transform:rotate(135deg)}.ant-steps-navigation>.ant-steps-item>.ant-steps-item-container>.ant-steps-item-tail{visibility:hidden}}.ant-steps-flex-not-supported.ant-steps-horizontal.ant-steps-label-horizontal .ant-steps-item{margin-left:-16px;padding-left:16px;background:#fff}.ant-steps-flex-not-supported.ant-steps-horizontal.ant-steps-label-horizontal.ant-steps-small .ant-steps-item{margin-left:-12px;padding-left:12px}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item:last-child{overflow:hidden}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item:last-child .ant-steps-icon-dot:after{right:-200px;width:200px}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:after,.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:before{position:absolute;top:0;left:-10px;width:10px;height:8px;background:#fff;content:""}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item .ant-steps-icon-dot:after{right:-10px;left:auto}.ant-steps-flex-not-supported.ant-steps-dot .ant-steps-item-wait .ant-steps-item-icon>.ant-steps-icon .ant-steps-icon-dot{background:#ccc}.ant-switch{margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;min-width:44px;height:22px;line-height:20px;vertical-align:middle;background-color:rgba(0,0,0,.25);border:1px solid transparent;border-radius:100px;cursor:pointer;-webkit-transition:all .36s;transition:all .36s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-switch-inner{display:block;margin-right:6px;margin-left:24px;color:#fff;font-size:12px}.ant-switch-loading-icon,.ant-switch:after{position:absolute;top:1px;left:1px;width:18px;height:18px;background-color:#fff;border-radius:18px;cursor:pointer;-webkit-transition:all .36s cubic-bezier(.78,.14,.15,.86);transition:all .36s cubic-bezier(.78,.14,.15,.86);content:" "}.ant-switch:after{-webkit-box-shadow:0 2px 4px 0 rgba(0,35,11,.2);box-shadow:0 2px 4px 0 rgba(0,35,11,.2)}.ant-switch:not(.ant-switch-disabled):active:after,.ant-switch:not(.ant-switch-disabled):active:before{width:24px}.ant-switch-loading-icon{z-index:1;display:none;font-size:12px;background:transparent}.ant-switch-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-switch-loading .ant-switch-loading-icon{display:inline-block;color:rgba(0,0,0,.65)}.ant-switch-checked.ant-switch-loading .ant-switch-loading-icon{color:#1890ff}.ant-switch:focus{outline:0;-webkit-box-shadow:0 0 0 2px rgba(24,144,255,.2);box-shadow:0 0 0 2px rgba(24,144,255,.2)}.ant-switch:focus:hover{-webkit-box-shadow:none;box-shadow:none}.ant-switch-small{min-width:28px;height:16px;line-height:14px}.ant-switch-small .ant-switch-inner{margin-right:3px;margin-left:18px;font-size:12px}.ant-switch-small:after{width:12px;height:12px}.ant-switch-small:active:after,.ant-switch-small:active:before{width:16px}.ant-switch-small .ant-switch-loading-icon{width:12px;height:12px}.ant-switch-small.ant-switch-checked .ant-switch-inner{margin-right:18px;margin-left:3px}.ant-switch-small.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-13px}.ant-switch-small.ant-switch-loading .ant-switch-loading-icon{font-weight:700;-webkit-transform:scale(.66667);-ms-transform:scale(.66667);transform:scale(.66667)}.ant-switch-checked{background-color:#1890ff}.ant-switch-checked .ant-switch-inner{margin-right:24px;margin-left:6px}.ant-switch-checked:after{left:100%;margin-left:-1px;-webkit-transform:translateX(-100%);-ms-transform:translateX(-100%);transform:translateX(-100%)}.ant-switch-checked .ant-switch-loading-icon{left:100%;margin-left:-19px}.ant-switch-disabled,.ant-switch-loading{cursor:not-allowed;opacity:.4}.ant-switch-disabled *,.ant-switch-disabled:after,.ant-switch-disabled:before,.ant-switch-loading *,.ant-switch-loading:after,.ant-switch-loading:before{cursor:not-allowed}@-webkit-keyframes AntSwitchSmallLoadingCircle{0%{-webkit-transform:rotate(0deg) scale(.66667);transform:rotate(0deg) scale(.66667);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}to{-webkit-transform:rotate(1turn) scale(.66667);transform:rotate(1turn) scale(.66667);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}}@keyframes AntSwitchSmallLoadingCircle{0%{-webkit-transform:rotate(0deg) scale(.66667);transform:rotate(0deg) scale(.66667);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}to{-webkit-transform:rotate(1turn) scale(.66667);transform:rotate(1turn) scale(.66667);-webkit-transform-origin:50% 50%;transform-origin:50% 50%}}.ant-table-wrapper{zoom:1}.ant-table-wrapper:after,.ant-table-wrapper:before{display:table;content:""}.ant-table-wrapper:after{clear:both}.ant-table{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;clear:both}.ant-table-body{-webkit-transition:opacity .3s;transition:opacity .3s}.ant-table-empty .ant-table-body{overflow-x:auto!important;overflow-y:hidden!important}.ant-table table{width:100%;text-align:left;border-radius:4px 4px 0 0;border-collapse:separate;border-spacing:0}.ant-table-layout-fixed table{table-layout:fixed}.ant-table-thead>tr>th{color:rgba(0,0,0,.85);font-weight:500;text-align:left;background:#fafafa;border-bottom:1px solid #e8e8e8;-webkit-transition:background .3s ease;transition:background .3s ease}.ant-table-thead>tr>th[colspan]:not([colspan="1"]){text-align:center}.ant-table-thead>tr>th .ant-table-filter-icon,.ant-table-thead>tr>th .anticon-filter{position:absolute;top:0;right:0;width:28px;height:100%;color:#bfbfbf;font-size:12px;text-align:center;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-table-thead>tr>th .ant-table-filter-icon>svg,.ant-table-thead>tr>th .anticon-filter>svg{position:absolute;top:50%;left:50%;margin-top:-5px;margin-left:-6px}.ant-table-thead>tr>th .ant-table-filter-selected.anticon{color:#1890ff}.ant-table-thead>tr>th .ant-table-column-sorter{display:table-cell;vertical-align:middle}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner{height:1em;margin-top:.35em;margin-left:.57142857em;color:#bfbfbf;line-height:1em;text-align:center;-webkit-transition:all .3s;transition:all .3s}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up{display:inline-block;font-size:12px;font-size:11px\9;-webkit-transform:scale(.91666667) rotate(0deg);-ms-transform:scale(.91666667) rotate(0deg);transform:scale(.91666667) rotate(0deg);display:block;height:1em;line-height:1em;-webkit-transition:all .3s;transition:all .3s}:root .ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down,:root .ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up{font-size:12px}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-down.on,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner .ant-table-column-sorter-up.on{color:#1890ff}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full{margin-top:-.15em}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-down,.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-up{height:.5em;line-height:.5em}.ant-table-thead>tr>th .ant-table-column-sorter .ant-table-column-sorter-inner-full .ant-table-column-sorter-down{margin-top:.125em}.ant-table-thead>tr>th.ant-table-column-has-actions{position:relative;background-clip:padding-box;-webkit-background-clip:border-box}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters{padding-right:30px!important}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters .ant-table-filter-icon.ant-table-filter-open,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters .anticon-filter.ant-table-filter-open,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:hover,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:hover{color:rgba(0,0,0,.45);background:#e5e5e5}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .ant-table-filter-icon:active,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-filters:hover .anticon-filter:active{color:rgba(0,0,0,.65)}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters{cursor:pointer}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .ant-table-filter-icon,.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:hover .anticon-filter{background:#f2f2f2}.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-down:not(.on),.ant-table-thead>tr>th.ant-table-column-has-actions.ant-table-column-has-sorters:active .ant-table-column-sorter-up:not(.on){color:rgba(0,0,0,.45)}.ant-table-thead>tr>th .ant-table-header-column{display:inline-block;max-width:100%;vertical-align:top}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters{display:table}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters>.ant-table-column-title{display:table-cell;vertical-align:middle}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters>:not(.ant-table-column-sorter){position:relative}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters:before{position:absolute;top:0;right:0;bottom:0;left:0;background:transparent;-webkit-transition:all .3s;transition:all .3s;content:""}.ant-table-thead>tr>th .ant-table-header-column .ant-table-column-sorters:hover:before{background:rgba(0,0,0,.04)}.ant-table-thead>tr>th.ant-table-column-has-sorters{-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-table-thead>tr:first-child>th:first-child{border-top-left-radius:4px}.ant-table-thead>tr:first-child>th:last-child{border-top-right-radius:4px}.ant-table-thead>tr:not(:last-child)>th[colspan]{border-bottom:0}.ant-table-tbody>tr>td{border-bottom:1px solid #e8e8e8;-webkit-transition:all .3s,border 0s;transition:all .3s,border 0s}.ant-table-tbody>tr,.ant-table-thead>tr{-webkit-transition:all .3s,height 0s;transition:all .3s,height 0s}.ant-table-tbody>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td{background:#e6f7ff}.ant-table-tbody>tr.ant-table-row-selected>td.ant-table-column-sort,.ant-table-tbody>tr:hover.ant-table-row-selected>td,.ant-table-tbody>tr:hover.ant-table-row-selected>td.ant-table-column-sort,.ant-table-thead>tr.ant-table-row-selected>td.ant-table-column-sort,.ant-table-thead>tr:hover.ant-table-row-selected>td,.ant-table-thead>tr:hover.ant-table-row-selected>td.ant-table-column-sort{background:#fafafa}.ant-table-thead>tr:hover{background:none}.ant-table-footer{position:relative;padding:16px;color:rgba(0,0,0,.85);background:#fafafa;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px}.ant-table-footer:before{position:absolute;top:-1px;left:0;width:100%;height:1px;background:#fafafa;content:""}.ant-table.ant-table-bordered .ant-table-footer{border:1px solid #e8e8e8}.ant-table-title{position:relative;top:1px;padding:16px 0;border-radius:4px 4px 0 0}.ant-table.ant-table-bordered .ant-table-title{padding-right:16px;padding-left:16px;border:1px solid #e8e8e8}.ant-table-title+.ant-table-content{position:relative;border-radius:4px 4px 0 0}.ant-table-bordered .ant-table-title+.ant-table-content,.ant-table-bordered .ant-table-title+.ant-table-content .ant-table-thead>tr:first-child>th,.ant-table-bordered .ant-table-title+.ant-table-content table,.ant-table-without-column-header .ant-table-title+.ant-table-content,.ant-table-without-column-header table{border-radius:0}.ant-table-without-column-header.ant-table-bordered.ant-table-empty .ant-table-placeholder{border-top:1px solid #e8e8e8;border-radius:4px}.ant-table-tbody>tr.ant-table-row-selected td{color:inherit;background:#fafafa}.ant-table-thead>tr>th.ant-table-column-sort{background:#f5f5f5}.ant-table-tbody>tr>td.ant-table-column-sort{background:rgba(0,0,0,.01)}.ant-table-tbody>tr>td,.ant-table-thead>tr>th{padding:16px;overflow-wrap:break-word}.ant-table-expand-icon-th,.ant-table-row-expand-icon-cell{width:50px;min-width:50px;text-align:center}.ant-table-header{overflow:hidden;background:#fafafa}.ant-table-header table{border-radius:4px 4px 0 0}.ant-table-loading{position:relative}.ant-table-loading .ant-table-body{background:#fff;opacity:.5}.ant-table-loading .ant-table-spin-holder{position:absolute;top:50%;left:50%;height:20px;margin-left:-30px;line-height:20px}.ant-table-loading .ant-table-with-pagination{margin-top:-20px}.ant-table-loading .ant-table-without-pagination{margin-top:10px}.ant-table-bordered .ant-table-body>table,.ant-table-bordered .ant-table-fixed-left table,.ant-table-bordered .ant-table-fixed-right table,.ant-table-bordered .ant-table-header>table{border:1px solid #e8e8e8;border-right:0;border-bottom:0}.ant-table-bordered.ant-table-empty .ant-table-placeholder{border-right:1px solid #e8e8e8;border-left:1px solid #e8e8e8}.ant-table-bordered.ant-table-fixed-header .ant-table-header>table{border-bottom:0}.ant-table-bordered.ant-table-fixed-header .ant-table-body>table{border-top-left-radius:0;border-top-right-radius:0}.ant-table-bordered.ant-table-fixed-header .ant-table-body-inner>table,.ant-table-bordered.ant-table-fixed-header .ant-table-header+.ant-table-body>table{border-top:0}.ant-table-bordered .ant-table-thead>tr:not(:last-child)>th{border-bottom:1px solid #e8e8e8}.ant-table-bordered .ant-table-tbody>tr>td,.ant-table-bordered .ant-table-thead>tr>th{border-right:1px solid #e8e8e8}.ant-table-placeholder{position:relative;z-index:1;margin-top:-1px;padding:16px;color:rgba(0,0,0,.25);font-size:14px;text-align:center;background:#fff;border-top:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8;border-radius:0 0 4px 4px}.ant-table-pagination.ant-pagination{float:right;margin:16px 0}.ant-table-filter-dropdown{position:relative;min-width:96px;margin-left:-8px;background:#fff;border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-filter-dropdown .ant-dropdown-menu{max-height:calc(100vh - 130px);overflow-x:hidden;border:0;border-radius:4px 4px 0 0;-webkit-box-shadow:none;box-shadow:none}.ant-table-filter-dropdown .ant-dropdown-menu-item>label+span{padding-right:0}.ant-table-filter-dropdown .ant-dropdown-menu-sub{border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-filter-dropdown .ant-dropdown-menu .ant-dropdown-submenu-contain-selected .ant-dropdown-menu-submenu-title:after{color:#1890ff;font-weight:700;text-shadow:0 0 2px #bae7ff}.ant-table-filter-dropdown .ant-dropdown-menu-item{overflow:hidden}.ant-table-filter-dropdown>.ant-dropdown-menu>.ant-dropdown-menu-item:last-child,.ant-table-filter-dropdown>.ant-dropdown-menu>.ant-dropdown-menu-submenu:last-child .ant-dropdown-menu-submenu-title{border-radius:0}.ant-table-filter-dropdown-btns{padding:7px 8px;overflow:hidden;border-top:1px solid #e8e8e8}.ant-table-filter-dropdown-link{color:#1890ff}.ant-table-filter-dropdown-link:hover{color:#40a9ff}.ant-table-filter-dropdown-link:active{color:#096dd9}.ant-table-filter-dropdown-link.confirm{float:left}.ant-table-filter-dropdown-link.clear{float:right}.ant-table-selection{white-space:nowrap}.ant-table-selection-select-all-custom{margin-right:4px!important}.ant-table-selection .anticon-down{color:#bfbfbf;-webkit-transition:all .3s;transition:all .3s}.ant-table-selection-menu{min-width:96px;margin-top:5px;margin-left:-30px;background:#fff;border-radius:4px;-webkit-box-shadow:0 2px 8px rgba(0,0,0,.15);box-shadow:0 2px 8px rgba(0,0,0,.15)}.ant-table-selection-menu .ant-action-down{color:#bfbfbf}.ant-table-selection-down{display:inline-block;padding:0;line-height:1;cursor:pointer}.ant-table-selection-down:hover .anticon-down{color:rgba(0,0,0,.6)}.ant-table-row-expand-icon{color:#1890ff;text-decoration:none;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;display:inline-block;width:17px;height:17px;color:inherit;line-height:13px;text-align:center;background:#fff;border:1px solid #e8e8e8;border-radius:2px;outline:none;-webkit-transition:all .3s;transition:all .3s;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{color:#40a9ff}.ant-table-row-expand-icon:active{color:#096dd9}.ant-table-row-expand-icon:active,.ant-table-row-expand-icon:focus,.ant-table-row-expand-icon:hover{border-color:currentColor}.ant-table-row-expanded:after{content:"-"}.ant-table-row-collapsed:after{content:"+"}.ant-table-row-spaced{visibility:hidden}.ant-table-row-spaced:after{content:"."}.ant-table-row-cell-ellipsis,.ant-table-row-cell-ellipsis .ant-table-column-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-table-row-cell-ellipsis .ant-table-column-title{display:block}.ant-table-row-cell-break-word{word-wrap:break-word;word-break:break-word}tr.ant-table-expanded-row,tr.ant-table-expanded-row:hover{background:#fbfbfb}tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-16px -16px -17px}.ant-table .ant-table-row-indent+.ant-table-row-expand-icon{margin-right:8px}.ant-table-scroll{overflow:auto;overflow-x:hidden}.ant-table-scroll table{min-width:100%}.ant-table-scroll table .ant-table-fixed-columns-in-body:not([colspan]){color:transparent}.ant-table-scroll table .ant-table-fixed-columns-in-body:not([colspan])>*{visibility:hidden}.ant-table-body-inner{height:100%}.ant-table-fixed-header>.ant-table-content>.ant-table-scroll>.ant-table-body{position:relative;background:#fff}.ant-table-fixed-header .ant-table-body-inner{overflow:scroll}.ant-table-fixed-header .ant-table-scroll .ant-table-header{margin-bottom:-20px;padding-bottom:20px;overflow:scroll;opacity:.9999}.ant-table-fixed-header .ant-table-scroll .ant-table-header::-webkit-scrollbar{border:solid #e8e8e8;border-width:0 0 1px}.ant-table-hide-scrollbar{scrollbar-color:transparent transparent;min-width:unset}.ant-table-hide-scrollbar::-webkit-scrollbar{min-width:inherit;background-color:transparent}.ant-table-bordered.ant-table-fixed-header .ant-table-scroll .ant-table-header::-webkit-scrollbar{border:1px solid #e8e8e8;border-left-width:0}.ant-table-bordered.ant-table-fixed-header .ant-table-scroll .ant-table-header.ant-table-hide-scrollbar .ant-table-thead>tr:only-child>th:last-child{border-right-color:transparent}.ant-table-fixed-left,.ant-table-fixed-right{position:absolute;top:0;z-index:1;overflow:hidden;border-radius:0;-webkit-transition:-webkit-box-shadow .3s ease;transition:-webkit-box-shadow .3s ease;transition:box-shadow .3s ease;transition:box-shadow .3s ease,-webkit-box-shadow .3s ease}.ant-table-fixed-left table,.ant-table-fixed-right table{width:auto;background:#fff}.ant-table-fixed-header .ant-table-fixed-left .ant-table-body-outer .ant-table-fixed,.ant-table-fixed-header .ant-table-fixed-right .ant-table-body-outer .ant-table-fixed{border-radius:0}.ant-table-fixed-left{left:0;-webkit-box-shadow:6px 0 6px -4px rgba(0,0,0,.15);box-shadow:6px 0 6px -4px rgba(0,0,0,.15)}.ant-table-fixed-left .ant-table-header{overflow-y:hidden}.ant-table-fixed-left .ant-table-body-inner{margin-right:-20px;padding-right:20px}.ant-table-fixed-header .ant-table-fixed-left .ant-table-body-inner{padding-right:0}.ant-table-fixed-left,.ant-table-fixed-left table{border-radius:4px 0 0 0}.ant-table-fixed-left .ant-table-thead>tr>th:last-child{border-top-right-radius:0}.ant-table-fixed-right{right:0;-webkit-box-shadow:-6px 0 6px -4px rgba(0,0,0,.15);box-shadow:-6px 0 6px -4px rgba(0,0,0,.15)}.ant-table-fixed-right,.ant-table-fixed-right table{border-radius:0 4px 0 0}.ant-table-fixed-right .ant-table-expanded-row{color:transparent;pointer-events:none}.ant-table-fixed-right .ant-table-thead>tr>th:first-child{border-top-left-radius:0}.ant-table.ant-table-scroll-position-left .ant-table-fixed-left,.ant-table.ant-table-scroll-position-right .ant-table-fixed-right{-webkit-box-shadow:none;box-shadow:none}.ant-table colgroup>col.ant-table-selection-col{width:60px}.ant-table-thead>tr>th.ant-table-selection-column-custom .ant-table-selection{margin-right:-15px}.ant-table-tbody>tr>td.ant-table-selection-column,.ant-table-thead>tr>th.ant-table-selection-column{text-align:center}.ant-table-tbody>tr>td.ant-table-selection-column .ant-radio-wrapper,.ant-table-thead>tr>th.ant-table-selection-column .ant-radio-wrapper{margin-right:0}.ant-table-row[class*=ant-table-row-level-0] .ant-table-selection-column>span{display:inline-block}.ant-table-filter-dropdown-submenu .ant-checkbox-wrapper+span,.ant-table-filter-dropdown .ant-checkbox-wrapper+span{padding-left:8px}@supports (-moz-appearance:meterbar){.ant-table-thead>tr>th.ant-table-column-has-actions{background-clip:padding-box}}.ant-table-middle>.ant-table-content>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-footer,.ant-table-middle>.ant-table-content>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-middle>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-middle>.ant-table-title{padding:12px 8px}.ant-table-middle tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-12px -8px -13px}.ant-table-small{border:1px solid #e8e8e8;border-radius:4px}.ant-table-small>.ant-table-content>.ant-table-footer,.ant-table-small>.ant-table-title{padding:8px}.ant-table-small>.ant-table-title{top:0;border-bottom:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-footer{background-color:transparent;border-top:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-footer:before{background-color:transparent}.ant-table-small>.ant-table-content>.ant-table-body{margin:0 8px}.ant-table-small>.ant-table-content>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table{border:0}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-tbody>tr>td,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th{padding:8px}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th{background-color:transparent}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr{border-bottom:1px solid #e8e8e8}.ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table>.ant-table-thead>tr>th.ant-table-column-sort,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table>.ant-table-thead>tr>th.ant-table-column-sort{background-color:rgba(0,0,0,.01)}.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-left>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-body-outer>.ant-table-body-inner>table,.ant-table-small>.ant-table-content>.ant-table-fixed-right>.ant-table-header>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-body>table,.ant-table-small>.ant-table-content>.ant-table-scroll>.ant-table-header>table{padding:0}.ant-table-small>.ant-table-content .ant-table-header{background-color:transparent;border-radius:4px 4px 0 0}.ant-table-small>.ant-table-content .ant-table-placeholder,.ant-table-small>.ant-table-content .ant-table-row:last-child td{border-bottom:0}.ant-table-small.ant-table-bordered{border-right:0}.ant-table-small.ant-table-bordered .ant-table-title{border:0;border-right:1px solid #e8e8e8;border-bottom:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-content{border-right:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-footer{border:0;border-top:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-footer:before{display:none}.ant-table-small.ant-table-bordered .ant-table-placeholder{border-right:0;border-bottom:0;border-left:0}.ant-table-small.ant-table-bordered .ant-table-tbody>tr>td:last-child,.ant-table-small.ant-table-bordered .ant-table-thead>tr>th.ant-table-row-cell-last{border-right:none}.ant-table-small.ant-table-bordered .ant-table-fixed-left .ant-table-tbody>tr>td:last-child,.ant-table-small.ant-table-bordered .ant-table-fixed-left .ant-table-thead>tr>th:last-child{border-right:1px solid #e8e8e8}.ant-table-small.ant-table-bordered .ant-table-fixed-right{border-right:1px solid #e8e8e8;border-left:1px solid #e8e8e8}.ant-table-small tr.ant-table-expanded-row td>.ant-table-wrapper{margin:-8px -8px -9px}.ant-table-small.ant-table-fixed-header>.ant-table-content>.ant-table-scroll>.ant-table-body{border-radius:0 0 4px 4px}.ant-timeline{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";margin:0;padding:0;list-style:none}.ant-timeline-item{position:relative;margin:0;padding:0 0 20px;font-size:14px;list-style:none}.ant-timeline-item-tail{position:absolute;top:10px;left:4px;height:calc(100% - 10px);border-left:2px solid #e8e8e8}.ant-timeline-item-pending .ant-timeline-item-head{font-size:12px;background-color:transparent}.ant-timeline-item-pending .ant-timeline-item-tail{display:none}.ant-timeline-item-head{position:absolute;width:10px;height:10px;background-color:#fff;border:2px solid transparent;border-radius:100px}.ant-timeline-item-head-blue{color:#1890ff;border-color:#1890ff}.ant-timeline-item-head-red{color:#f5222d;border-color:#f5222d}.ant-timeline-item-head-green{color:#52c41a;border-color:#52c41a}.ant-timeline-item-head-gray{color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-timeline-item-head-custom{position:absolute;top:5.5px;left:5px;width:auto;height:auto;margin-top:0;padding:3px 1px;line-height:1;text-align:center;border:0;border-radius:0;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ant-timeline-item-content{position:relative;top:-6px;margin:0 0 0 18px;word-break:break-word}.ant-timeline-item-last>.ant-timeline-item-tail{display:none}.ant-timeline-item-last>.ant-timeline-item-content{min-height:48px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head,.ant-timeline.ant-timeline-alternate .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-alternate .ant-timeline-item-tail,.ant-timeline.ant-timeline-right .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-tail{left:50%}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-head{margin-left:-4px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-head-custom{margin-left:1px}.ant-timeline.ant-timeline-alternate .ant-timeline-item-left .ant-timeline-item-content,.ant-timeline.ant-timeline-right .ant-timeline-item-left .ant-timeline-item-content{left:calc(50% - 4px);width:calc(50% - 14px);text-align:left}.ant-timeline.ant-timeline-alternate .ant-timeline-item-right .ant-timeline-item-content,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-content{width:calc(50% - 12px);margin:0;text-align:right}.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-head,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-head-custom,.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-tail{left:calc(100% - 6px)}.ant-timeline.ant-timeline-right .ant-timeline-item-right .ant-timeline-item-content{width:calc(100% - 18px)}.ant-timeline.ant-timeline-pending .ant-timeline-item-last .ant-timeline-item-tail{display:block;height:calc(100% - 14px);border-left:2px dotted #e8e8e8}.ant-timeline.ant-timeline-reverse .ant-timeline-item-last .ant-timeline-item-tail{display:none}.ant-timeline.ant-timeline-reverse .ant-timeline-item-pending .ant-timeline-item-tail{top:15px;display:block;height:calc(100% - 15px);border-left:2px dotted #e8e8e8}.ant-timeline.ant-timeline-reverse .ant-timeline-item-pending .ant-timeline-item-content{min-height:48px}.ant-transfer-customize-list{display:-ms-flexbox;display:flex}.ant-transfer-customize-list .ant-transfer-operation{-ms-flex:none;flex:none;-ms-flex-item-align:center;align-self:center}.ant-transfer-customize-list .ant-transfer-list{-ms-flex:auto;flex:auto;width:auto;height:auto;min-height:200px}.ant-transfer-customize-list .ant-transfer-list-body-with-search{padding-top:0}.ant-transfer-customize-list .ant-transfer-list-body-search-wrapper{position:relative;padding-bottom:0}.ant-transfer-customize-list .ant-transfer-list-body-customize-wrapper{padding:12px}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small{border:0;border-radius:0}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small>.ant-table-content>.ant-table-body>table>.ant-table-thead>tr>th{background:#fafafa}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small>.ant-table-content .ant-table-row:last-child td{border-bottom:1px solid #e8e8e8}.ant-transfer-customize-list .ant-table-wrapper .ant-table-small .ant-table-body{margin:0}.ant-transfer-customize-list .ant-table-wrapper .ant-table-pagination.ant-pagination{margin:16px 0 4px}.ant-transfer{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative}.ant-transfer-disabled .ant-transfer-list{background:#f5f5f5}.ant-transfer-list{position:relative;display:inline-block;width:180px;height:200px;padding-top:40px;vertical-align:middle;border:1px solid #d9d9d9;border-radius:4px}.ant-transfer-list-with-footer{padding-bottom:34px}.ant-transfer-list-search{padding:0 24px 0 8px}.ant-transfer-list-search-action{position:absolute;top:12px;right:12px;bottom:12px;width:28px;color:rgba(0,0,0,.25);line-height:32px;text-align:center}.ant-transfer-list-search-action .anticon{color:rgba(0,0,0,.25);-webkit-transition:all .3s;transition:all .3s}.ant-transfer-list-search-action .anticon:hover{color:rgba(0,0,0,.45)}span.ant-transfer-list-search-action{pointer-events:none}.ant-transfer-list-header{position:absolute;top:0;left:0;width:100%;padding:8px 12px 9px;overflow:hidden;color:rgba(0,0,0,.65);background:#fff;border-bottom:1px solid #e8e8e8;border-radius:4px 4px 0 0}.ant-transfer-list-header-title{position:absolute;right:12px}.ant-transfer-list-header .ant-checkbox-wrapper+span{padding-left:8px}.ant-transfer-list-body{position:relative;height:100%;font-size:14px}.ant-transfer-list-body-search-wrapper{position:absolute;top:0;left:0;width:100%;padding:12px}.ant-transfer-list-body-with-search{padding-top:56px}.ant-transfer-list-content{height:100%;margin:0;padding:0;overflow:auto;list-style:none}.ant-transfer-list-content>.LazyLoad{-webkit-animation:transferHighlightIn 1s;animation:transferHighlightIn 1s}.ant-transfer-list-content-item{min-height:32px;padding:6px 12px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-webkit-transition:all .3s;transition:all .3s}.ant-transfer-list-content-item>span{padding-right:0}.ant-transfer-list-content-item-text{padding-left:8px}.ant-transfer-list-content-item:not(.ant-transfer-list-content-item-disabled):hover{background-color:#e6f7ff;cursor:pointer}.ant-transfer-list-content-item-disabled{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-transfer-list-body-not-found{position:absolute;top:50%;width:100%;padding-top:0;color:rgba(0,0,0,.25);text-align:center;-webkit-transform:translateY(-50%);-ms-transform:translateY(-50%);transform:translateY(-50%)}.ant-transfer-list-body-with-search .ant-transfer-list-body-not-found{margin-top:16px}.ant-transfer-list-footer{position:absolute;bottom:0;left:0;width:100%;border-top:1px solid #e8e8e8;border-radius:0 0 4px 4px}.ant-transfer-operation{display:inline-block;margin:0 8px;overflow:hidden;vertical-align:middle}.ant-transfer-operation .ant-btn{display:block}.ant-transfer-operation .ant-btn:first-child{margin-bottom:4px}.ant-transfer-operation .ant-btn .anticon{font-size:12px}@-webkit-keyframes transferHighlightIn{0%{background:#bae7ff}to{background:transparent}}@keyframes transferHighlightIn{0%{background:#bae7ff}to{background:transparent}}.ant-select-tree-checkbox{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-select-tree-checkbox-input:focus+.ant-select-tree-checkbox-inner,.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox-inner,.ant-select-tree-checkbox:hover .ant-select-tree-checkbox-inner{border-color:#1890ff}.ant-select-tree-checkbox-checked:after{position:absolute;top:0;left:0;width:100%;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox:after,.ant-select-tree-checkbox:hover:after{visibility:visible}.ant-select-tree-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;-webkit-transition:all .3s;transition:all .3s}.ant-select-tree-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(0) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(0) translate(-50%,-50%);transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;-webkit-transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-select-tree-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(1) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(1) translate(-50%,-50%);transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;-webkit-transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner{background-color:#1890ff;border-color:#1890ff}.ant-select-tree-checkbox-disabled{cursor:not-allowed}.ant-select-tree-checkbox-disabled.ant-select-tree-checkbox-checked .ant-select-tree-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-input{cursor:not-allowed}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;-webkit-animation-name:none;animation-name:none}.ant-select-tree-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-select-tree-checkbox-disabled:hover:after,.ant-select-tree-checkbox-wrapper:hover .ant-select-tree-checkbox-disabled:after{visibility:hidden}.ant-select-tree-checkbox-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-select-tree-checkbox-wrapper.ant-select-tree-checkbox-wrapper-disabled{cursor:not-allowed}.ant-select-tree-checkbox-wrapper+.ant-select-tree-checkbox-wrapper{margin-left:8px}.ant-select-tree-checkbox+span{padding-right:8px;padding-left:8px}.ant-select-tree-checkbox-group{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block}.ant-select-tree-checkbox-group-item{display:inline-block;margin-right:8px}.ant-select-tree-checkbox-group-item:last-child{margin-right:0}.ant-select-tree-checkbox-group-item+.ant-select-tree-checkbox-group-item{margin-left:0}.ant-select-tree-checkbox-indeterminate .ant-select-tree-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-select-tree-checkbox-indeterminate .ant-select-tree-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;-webkit-transform:translate(-50%,-50%) scale(1);-ms-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-select-tree-checkbox-indeterminate.ant-select-tree-checkbox-disabled .ant-select-tree-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-select-tree{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";margin:-4px 0 0;padding:0 4px}.ant-select-tree li{margin:8px 0;padding:0;white-space:nowrap;list-style:none;outline:0}.ant-select-tree li.filter-node>span{font-weight:500}.ant-select-tree li ul{margin:0;padding:0 0 0 18px}.ant-select-tree li .ant-select-tree-node-content-wrapper{display:inline-block;width:calc(100% - 24px);margin:0;padding:3px 5px;color:rgba(0,0,0,.65);text-decoration:none;border-radius:2px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-select-tree li .ant-select-tree-node-content-wrapper:hover{background-color:#e6f7ff}.ant-select-tree li .ant-select-tree-node-content-wrapper.ant-select-tree-node-selected{background-color:#bae7ff}.ant-select-tree li span.ant-select-tree-checkbox{margin:0 4px 0 0}.ant-select-tree li span.ant-select-tree-checkbox+.ant-select-tree-node-content-wrapper{width:calc(100% - 46px)}.ant-select-tree li span.ant-select-tree-iconEle,.ant-select-tree li span.ant-select-tree-switcher{display:inline-block;width:24px;height:24px;margin:0;line-height:22px;text-align:center;vertical-align:middle;border:0;outline:none;cursor:pointer}.ant-select-tree li span.ant-select-icon_loading .ant-select-switcher-loading-icon{position:absolute;left:0;display:inline-block;color:#1890ff;font-size:14px;-webkit-transform:none;-ms-transform:none;transform:none}.ant-select-tree li span.ant-select-icon_loading .ant-select-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-select-tree li span.ant-select-tree-switcher{position:relative}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher-noop{cursor:auto}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon,:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon{font-size:12px}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon,:root .ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon{font-size:12px}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-icon svg{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-loading-icon,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-loading-icon{position:absolute;left:0;display:inline-block;width:24px;height:24px;color:#1890ff;font-size:14px;-webkit-transform:none;-ms-transform:none;transform:none}.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_close .ant-select-switcher-loading-icon svg,.ant-select-tree li span.ant-select-tree-switcher.ant-select-tree-switcher_open .ant-select-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}.ant-select-tree-child-tree,.ant-select-tree .ant-select-tree-treenode-loading .ant-select-tree-iconEle{display:none}.ant-select-tree-child-tree-open{display:block}li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper,li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper span,li.ant-select-tree-treenode-disabled>span:not(.ant-select-tree-switcher){color:rgba(0,0,0,.25);cursor:not-allowed}li.ant-select-tree-treenode-disabled>.ant-select-tree-node-content-wrapper:hover{background:transparent}.ant-select-tree-icon__close,.ant-select-tree-icon__open{margin-right:2px;vertical-align:top}.ant-select-tree-dropdown{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum"}.ant-select-tree-dropdown .ant-select-dropdown-search{position:sticky;top:0;z-index:1;display:block;padding:4px;background:#fff}.ant-select-tree-dropdown .ant-select-dropdown-search .ant-select-search__field__wrap{width:100%}.ant-select-tree-dropdown .ant-select-dropdown-search .ant-select-search__field{-webkit-box-sizing:border-box;box-sizing:border-box;width:100%;padding:4px 7px;border:1px solid #d9d9d9;border-radius:4px;outline:none}.ant-select-tree-dropdown .ant-select-dropdown-search.ant-select-search--hide{display:none}.ant-select-tree-dropdown .ant-select-not-found{display:block;padding:7px 16px;color:rgba(0,0,0,.25);cursor:not-allowed}@-webkit-keyframes antCheckboxEffect{0%{-webkit-transform:scale(1);transform:scale(1);opacity:.5}to{-webkit-transform:scale(1.6);transform:scale(1.6);opacity:0}}@keyframes antCheckboxEffect{0%{-webkit-transform:scale(1);transform:scale(1);opacity:.5}to{-webkit-transform:scale(1.6);transform:scale(1.6);opacity:0}}.ant-tree.ant-tree-directory{position:relative}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-switcher,.ant-tree.ant-tree-directory>li span.ant-tree-switcher{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-switcher.ant-tree-switcher-noop,.ant-tree.ant-tree-directory>li span.ant-tree-switcher.ant-tree-switcher-noop{pointer-events:none}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-checkbox,.ant-tree.ant-tree-directory>li span.ant-tree-checkbox{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper{border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:hover,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:hover{background:transparent}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:hover:before,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:hover:before{background:#e6f7ff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper.ant-tree-node-selected,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper.ant-tree-node-selected{color:#fff;background:transparent}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper:before,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper:before{position:absolute;right:0;left:0;height:24px;-webkit-transition:all .3s;transition:all .3s;content:""}.ant-tree.ant-tree-directory .ant-tree-child-tree>li span.ant-tree-node-content-wrapper>span,.ant-tree.ant-tree-directory>li span.ant-tree-node-content-wrapper>span{position:relative;z-index:1}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-switcher,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-switcher{color:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox .ant-tree-checkbox-inner,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox .ant-tree-checkbox-inner{border-color:#1890ff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked:after,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked:after{border-color:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner{background:#fff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-checkbox.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{border-color:#1890ff}.ant-tree.ant-tree-directory .ant-tree-child-tree>li.ant-tree-treenode-selected>span.ant-tree-node-content-wrapper:before,.ant-tree.ant-tree-directory>li.ant-tree-treenode-selected>span.ant-tree-node-content-wrapper:before{background:#1890ff}.ant-tree-checkbox{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";position:relative;top:-.09em;display:inline-block;line-height:1;white-space:nowrap;vertical-align:middle;outline:none;cursor:pointer}.ant-tree-checkbox-input:focus+.ant-tree-checkbox-inner,.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox-inner,.ant-tree-checkbox:hover .ant-tree-checkbox-inner{border-color:#1890ff}.ant-tree-checkbox-checked:after{top:0;height:100%;border:1px solid #1890ff;border-radius:2px;visibility:hidden;-webkit-animation:antCheckboxEffect .36s ease-in-out;animation:antCheckboxEffect .36s ease-in-out;-webkit-animation-fill-mode:backwards;animation-fill-mode:backwards;content:""}.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox:after,.ant-tree-checkbox:hover:after{visibility:visible}.ant-tree-checkbox-inner{position:relative;top:0;left:0;display:block;width:16px;height:16px;background-color:#fff;border:1px solid #d9d9d9;border-radius:2px;border-collapse:separate;-webkit-transition:all .3s;transition:all .3s}.ant-tree-checkbox-inner:after{position:absolute;top:50%;left:22%;display:table;width:5.71428571px;height:9.14285714px;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(0) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(0) translate(-50%,-50%);transform:rotate(45deg) scale(0) translate(-50%,-50%);opacity:0;-webkit-transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;transition:all .1s cubic-bezier(.71,-.46,.88,.6),opacity .1s;content:" "}.ant-tree-checkbox-input{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;width:100%;height:100%;cursor:pointer;opacity:0}.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{position:absolute;display:table;border:2px solid #fff;border-top:0;border-left:0;-webkit-transform:rotate(45deg) scale(1) translate(-50%,-50%);-ms-transform:rotate(45deg) scale(1) translate(-50%,-50%);transform:rotate(45deg) scale(1) translate(-50%,-50%);opacity:1;-webkit-transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;transition:all .2s cubic-bezier(.12,.4,.29,1.46) .1s;content:" "}.ant-tree-checkbox-checked .ant-tree-checkbox-inner{background-color:#1890ff;border-color:#1890ff}.ant-tree-checkbox-disabled{cursor:not-allowed}.ant-tree-checkbox-disabled.ant-tree-checkbox-checked .ant-tree-checkbox-inner:after{border-color:rgba(0,0,0,.25);-webkit-animation-name:none;animation-name:none}.ant-tree-checkbox-disabled .ant-tree-checkbox-input{cursor:not-allowed}.ant-tree-checkbox-disabled .ant-tree-checkbox-inner{background-color:#f5f5f5;border-color:#d9d9d9!important}.ant-tree-checkbox-disabled .ant-tree-checkbox-inner:after{border-color:#f5f5f5;border-collapse:separate;-webkit-animation-name:none;animation-name:none}.ant-tree-checkbox-disabled+span{color:rgba(0,0,0,.25);cursor:not-allowed}.ant-tree-checkbox-disabled:hover:after,.ant-tree-checkbox-wrapper:hover .ant-tree-checkbox-disabled:after{visibility:hidden}.ant-tree-checkbox-wrapper{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block;line-height:unset;cursor:pointer}.ant-tree-checkbox-wrapper.ant-tree-checkbox-wrapper-disabled{cursor:not-allowed}.ant-tree-checkbox-wrapper+.ant-tree-checkbox-wrapper{margin-left:8px}.ant-tree-checkbox+span{padding-right:8px;padding-left:8px}.ant-tree-checkbox-group{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";display:inline-block}.ant-tree-checkbox-group-item{display:inline-block;margin-right:8px}.ant-tree-checkbox-group-item:last-child{margin-right:0}.ant-tree-checkbox-group-item+.ant-tree-checkbox-group-item{margin-left:0}.ant-tree-checkbox-indeterminate .ant-tree-checkbox-inner{background-color:#fff;border-color:#d9d9d9}.ant-tree-checkbox-indeterminate .ant-tree-checkbox-inner:after{top:50%;left:50%;width:8px;height:8px;background-color:#1890ff;border:0;-webkit-transform:translate(-50%,-50%) scale(1);-ms-transform:translate(-50%,-50%) scale(1);transform:translate(-50%,-50%) scale(1);opacity:1;content:" "}.ant-tree-checkbox-indeterminate.ant-tree-checkbox-disabled .ant-tree-checkbox-inner:after{background-color:rgba(0,0,0,.25);border-color:rgba(0,0,0,.25)}.ant-tree{-webkit-box-sizing:border-box;box-sizing:border-box;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";margin:0;padding:0}.ant-tree-checkbox-checked:after{position:absolute;top:16.67%;left:0;width:100%;height:66.67%}.ant-tree ol,.ant-tree ul{margin:0;padding:0;list-style:none}.ant-tree li{margin:0;padding:4px 0;white-space:nowrap;list-style:none;outline:0}.ant-tree li span[draggable=true],.ant-tree li span[draggable]{line-height:20px;border-top:2px solid transparent;border-bottom:2px solid transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-khtml-user-drag:element;-webkit-user-drag:element}.ant-tree li.drag-over>span[draggable]{color:#fff;background-color:#1890ff;opacity:.8}.ant-tree li.drag-over-gap-top>span[draggable]{border-top-color:#1890ff}.ant-tree li.drag-over-gap-bottom>span[draggable]{border-bottom-color:#1890ff}.ant-tree li.filter-node>span{color:#f5222d!important;font-weight:500!important}.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-loading-icon,.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-loading-icon{position:absolute;left:0;display:inline-block;width:24px;height:24px;color:#1890ff;font-size:14px;-webkit-transform:none;-ms-transform:none;transform:none}.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-loading-icon svg,.ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-loading-icon svg{position:absolute;top:0;right:0;bottom:0;left:0;margin:auto}:root .ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_close:after,:root .ant-tree li.ant-tree-treenode-loading span.ant-tree-switcher.ant-tree-switcher_open:after{opacity:0}.ant-tree li ul{margin:0;padding:0 0 0 18px}.ant-tree li .ant-tree-node-content-wrapper{display:inline-block;height:24px;margin:0;padding:0 5px;color:rgba(0,0,0,.65);line-height:24px;text-decoration:none;vertical-align:top;border-radius:2px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-tree li .ant-tree-node-content-wrapper:hover{background-color:#e6f7ff}.ant-tree li .ant-tree-node-content-wrapper.ant-tree-node-selected{background-color:#bae7ff}.ant-tree li span.ant-tree-checkbox{top:auto;height:24px;margin:0 4px 0 2px;padding:4px 0}.ant-tree li span.ant-tree-iconEle,.ant-tree li span.ant-tree-switcher{display:inline-block;width:24px;height:24px;margin:0;line-height:24px;text-align:center;vertical-align:top;border:0;outline:none;cursor:pointer}.ant-tree li span.ant-tree-iconEle:empty{display:none}.ant-tree li span.ant-tree-switcher{position:relative}.ant-tree li span.ant-tree-switcher.ant-tree-switcher-noop{cursor:default}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{font-size:12px}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon svg,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);display:inline-block;font-weight:700}:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,:root .ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{font-size:12px}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon svg,.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-tree li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{-webkit-transform:rotate(-90deg);-ms-transform:rotate(-90deg);transform:rotate(-90deg)}.ant-tree li:last-child>span.ant-tree-iconEle:before,.ant-tree li:last-child>span.ant-tree-switcher:before{display:none}.ant-tree>li:first-child{padding-top:7px}.ant-tree>li:last-child{padding-bottom:7px}.ant-tree-child-tree>li:first-child{padding-top:8px}.ant-tree-child-tree>li:last-child{padding-bottom:0}li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper,li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper span,li.ant-tree-treenode-disabled>span:not(.ant-tree-switcher){color:rgba(0,0,0,.25);cursor:not-allowed}li.ant-tree-treenode-disabled>.ant-tree-node-content-wrapper:hover{background:transparent}.ant-tree-icon__close,.ant-tree-icon__open{margin-right:2px;vertical-align:top}.ant-tree.ant-tree-show-line li{position:relative}.ant-tree.ant-tree-show-line li span.ant-tree-switcher{color:rgba(0,0,0,.45);background:#fff}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher-noop .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_open .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon{display:inline-block;font-weight:400;font-size:12px}.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-select-switcher-icon svg,.ant-tree.ant-tree-show-line li span.ant-tree-switcher.ant-tree-switcher_close .ant-tree-switcher-icon svg{-webkit-transition:-webkit-transform .3s;transition:-webkit-transform .3s;transition:transform .3s;transition:transform .3s,-webkit-transform .3s}.ant-tree.ant-tree-show-line li:not(:last-child):before{position:absolute;left:12px;width:1px;height:100%;height:calc(100% - 22px);margin:22px 0 0;border-left:1px solid #d9d9d9;content:" "}.ant-tree.ant-tree-icon-hide .ant-tree-treenode-loading .ant-tree-iconEle{display:none}.ant-tree.ant-tree-block-node li .ant-tree-node-content-wrapper{width:calc(100% - 24px)}.ant-tree.ant-tree-block-node li span.ant-tree-checkbox+.ant-tree-node-content-wrapper{width:calc(100% - 46px)}.ant-typography{color:rgba(0,0,0,.65)}.ant-typography.ant-typography-secondary{color:rgba(0,0,0,.45)}.ant-typography.ant-typography-warning{color:#faad14}.ant-typography.ant-typography-danger{color:#f5222d}.ant-typography.ant-typography-disabled{color:rgba(0,0,0,.25);cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ant-typography p,div.ant-typography{margin-bottom:1em}.ant-typography h1,h1.ant-typography{margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:600;font-size:38px;line-height:1.23}.ant-typography h2,h2.ant-typography{margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:600;font-size:30px;line-height:1.35}.ant-typography h3,h3.ant-typography{margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:600;font-size:24px;line-height:1.35}.ant-typography h4,h4.ant-typography{margin-bottom:.5em;color:rgba(0,0,0,.85);font-weight:600;font-size:20px;line-height:1.4}.ant-typography+h1.ant-typography,.ant-typography+h2.ant-typography,.ant-typography+h3.ant-typography,.ant-typography+h4.ant-typography,.ant-typography div+h1,.ant-typography div+h2,.ant-typography div+h3,.ant-typography div+h4,.ant-typography h1+h1,.ant-typography h1+h2,.ant-typography h1+h3,.ant-typography h1+h4,.ant-typography h2+h1,.ant-typography h2+h2,.ant-typography h2+h3,.ant-typography h2+h4,.ant-typography h3+h1,.ant-typography h3+h2,.ant-typography h3+h3,.ant-typography h3+h4,.ant-typography h4+h1,.ant-typography h4+h2,.ant-typography h4+h3,.ant-typography h4+h4,.ant-typography li+h1,.ant-typography li+h2,.ant-typography li+h3,.ant-typography li+h4,.ant-typography p+h1,.ant-typography p+h2,.ant-typography p+h3,.ant-typography p+h4,.ant-typography ul+h1,.ant-typography ul+h2,.ant-typography ul+h3,.ant-typography ul+h4{margin-top:1.2em}span.ant-typography-ellipsis{display:inline-block}.ant-typography a{color:#1890ff;text-decoration:none;outline:none;cursor:pointer;-webkit-transition:color .3s;transition:color .3s}.ant-typography a:focus,.ant-typography a:hover{color:#40a9ff}.ant-typography a:active{color:#096dd9}.ant-typography a:active,.ant-typography a:hover{text-decoration:none}.ant-typography a[disabled]{color:rgba(0,0,0,.25);cursor:not-allowed;pointer-events:none}.ant-typography code{margin:0 .2em;padding:.2em .4em .1em;font-size:85%;background:rgba(0,0,0,.06);border:1px solid rgba(0,0,0,.06);border-radius:3px}.ant-typography mark{padding:0;background-color:#ffe58f}.ant-typography ins,.ant-typography u{text-decoration:underline;-webkit-text-decoration-skip:ink;text-decoration-skip-ink:auto}.ant-typography del,.ant-typography s{text-decoration:line-through}.ant-typography strong{font-weight:600}.ant-typography-copy,.ant-typography-edit,.ant-typography-expand{color:#1890ff;text-decoration:none;outline:none;cursor:pointer;-webkit-transition:color .3s;transition:color .3s;margin-left:8px}.ant-typography-copy:focus,.ant-typography-copy:hover,.ant-typography-edit:focus,.ant-typography-edit:hover,.ant-typography-expand:focus,.ant-typography-expand:hover{color:#40a9ff}.ant-typography-copy:active,.ant-typography-edit:active,.ant-typography-expand:active{color:#096dd9}.ant-typography-copy-success,.ant-typography-copy-success:focus,.ant-typography-copy-success:hover{color:#52c41a}.ant-typography-edit-content{position:relative}div.ant-typography-edit-content{left:-12px;margin-top:-5px;margin-bottom:calc(1em - 6px)}.ant-typography-edit-content-confirm{position:absolute;right:10px;bottom:8px;color:rgba(0,0,0,.45);pointer-events:none}.ant-typography-edit-content textarea{-moz-transition:none}.ant-typography ol,.ant-typography ul{margin:0 0 1em;padding:0}.ant-typography ol li,.ant-typography ul li{margin:0 0 0 20px;padding:0 0 0 4px}.ant-typography ul li{list-style-type:circle}.ant-typography ul li li{list-style-type:disc}.ant-typography ol li{list-style-type:decimal}.ant-typography-ellipsis-single-line{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-typography-ellipsis-multiple-line{display:-webkit-box;-webkit-line-clamp:3; /*! autoprefixer: ignore next */-webkit-box-orient:vertical;overflow:hidden}.ant-upload{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";outline:0}.ant-upload p{margin:0}.ant-upload-btn{display:block;width:100%;outline:none}.ant-upload input[type=file]{cursor:pointer}.ant-upload.ant-upload-select{display:inline-block}.ant-upload.ant-upload-disabled{cursor:not-allowed}.ant-upload.ant-upload-select-picture-card{display:table;float:left;width:104px;height:104px;margin-right:8px;margin-bottom:8px;text-align:center;vertical-align:top;background-color:#fafafa;border:1px dashed #d9d9d9;border-radius:4px;cursor:pointer;-webkit-transition:border-color .3s ease;transition:border-color .3s ease}.ant-upload.ant-upload-select-picture-card>.ant-upload{display:table-cell;width:100%;height:100%;padding:8px;text-align:center;vertical-align:middle}.ant-upload.ant-upload-select-picture-card:hover{border-color:#1890ff}.ant-upload.ant-upload-drag{position:relative;width:100%;height:100%;text-align:center;background:#fafafa;border:1px dashed #d9d9d9;border-radius:4px;cursor:pointer;-webkit-transition:border-color .3s;transition:border-color .3s}.ant-upload.ant-upload-drag .ant-upload{padding:16px 0}.ant-upload.ant-upload-drag.ant-upload-drag-hover:not(.ant-upload-disabled){border-color:#096dd9}.ant-upload.ant-upload-drag.ant-upload-disabled{cursor:not-allowed}.ant-upload.ant-upload-drag .ant-upload-btn{display:table;height:100%}.ant-upload.ant-upload-drag .ant-upload-drag-container{display:table-cell;vertical-align:middle}.ant-upload.ant-upload-drag:not(.ant-upload-disabled):hover{border-color:#40a9ff}.ant-upload.ant-upload-drag p.ant-upload-drag-icon{margin-bottom:20px}.ant-upload.ant-upload-drag p.ant-upload-drag-icon .anticon{color:#40a9ff;font-size:48px}.ant-upload.ant-upload-drag p.ant-upload-text{margin:0 0 4px;color:rgba(0,0,0,.85);font-size:16px}.ant-upload.ant-upload-drag p.ant-upload-hint{color:rgba(0,0,0,.45);font-size:14px}.ant-upload.ant-upload-drag .anticon-plus{color:rgba(0,0,0,.25);font-size:30px;-webkit-transition:all .3s;transition:all .3s}.ant-upload.ant-upload-drag .anticon-plus:hover,.ant-upload.ant-upload-drag:hover .anticon-plus{color:rgba(0,0,0,.45)}.ant-upload-picture-card-wrapper{zoom:1;display:inline-block;width:100%}.ant-upload-picture-card-wrapper:after,.ant-upload-picture-card-wrapper:before{display:table;content:""}.ant-upload-picture-card-wrapper:after{clear:both}.ant-upload-list{-webkit-box-sizing:border-box;box-sizing:border-box;margin:0;padding:0;color:rgba(0,0,0,.65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;-webkit-font-feature-settings:"tnum";font-feature-settings:"tnum","tnum";zoom:1}.ant-upload-list:after,.ant-upload-list:before{display:table;content:""}.ant-upload-list:after{clear:both}.ant-upload-list-item-list-type-text:hover .ant-upload-list-item-name-icon-count-1{padding-right:14px}.ant-upload-list-item-list-type-text:hover .ant-upload-list-item-name-icon-count-2{padding-right:28px}.ant-upload-list-item{position:relative;height:22px;margin-top:8px;font-size:14px}.ant-upload-list-item-name{display:inline-block;width:100%;padding-left:22px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.ant-upload-list-item-name-icon-count-1{padding-right:14px}.ant-upload-list-item-card-actions{position:absolute;right:0;opacity:0}.ant-upload-list-item-card-actions.picture{top:25px;line-height:1;opacity:1}.ant-upload-list-item-card-actions .anticon{padding-right:6px;color:rgba(0,0,0,.45)}.ant-upload-list-item-info{height:100%;padding:0 12px 0 4px;-webkit-transition:background-color .3s;transition:background-color .3s}.ant-upload-list-item-info>span{display:block;width:100%;height:100%}.ant-upload-list-item-info .anticon-loading,.ant-upload-list-item-info .anticon-paper-clip{position:absolute;top:5px;color:rgba(0,0,0,.45);font-size:14px}.ant-upload-list-item .anticon-close{display:inline-block;font-size:12px;font-size:10px\9;-webkit-transform:scale(.83333333) rotate(0deg);-ms-transform:scale(.83333333) rotate(0deg);transform:scale(.83333333) rotate(0deg);position:absolute;top:6px;right:4px;color:rgba(0,0,0,.45);line-height:0;cursor:pointer;opacity:0;-webkit-transition:all .3s;transition:all .3s}:root .ant-upload-list-item .anticon-close{font-size:12px}.ant-upload-list-item .anticon-close:hover{color:rgba(0,0,0,.65)}.ant-upload-list-item:hover .ant-upload-list-item-info{background-color:#e6f7ff}.ant-upload-list-item:hover .ant-upload-list-item-card-actions,.ant-upload-list-item:hover .anticon-close{opacity:1}.ant-upload-list-item-error,.ant-upload-list-item-error .ant-upload-list-item-name,.ant-upload-list-item-error .anticon-paper-clip{color:#f5222d}.ant-upload-list-item-error .ant-upload-list-item-card-actions{opacity:1}.ant-upload-list-item-error .ant-upload-list-item-card-actions .anticon{color:#f5222d}.ant-upload-list-item-progress{position:absolute;bottom:-12px;width:100%;padding-left:26px;font-size:14px;line-height:0}.ant-upload-list-picture-card .ant-upload-list-item,.ant-upload-list-picture .ant-upload-list-item{position:relative;height:66px;padding:8px;border:1px solid #d9d9d9;border-radius:4px}.ant-upload-list-picture-card .ant-upload-list-item:hover,.ant-upload-list-picture .ant-upload-list-item:hover{background:transparent}.ant-upload-list-picture-card .ant-upload-list-item-error,.ant-upload-list-picture .ant-upload-list-item-error{border-color:#f5222d}.ant-upload-list-picture-card .ant-upload-list-item-info,.ant-upload-list-picture .ant-upload-list-item-info{padding:0}.ant-upload-list-picture-card .ant-upload-list-item:hover .ant-upload-list-item-info,.ant-upload-list-picture .ant-upload-list-item:hover .ant-upload-list-item-info{background:transparent}.ant-upload-list-picture-card .ant-upload-list-item-uploading,.ant-upload-list-picture .ant-upload-list-item-uploading{border-style:dashed}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail,.ant-upload-list-picture .ant-upload-list-item-thumbnail{position:absolute;top:8px;left:8px;width:48px;height:48px;font-size:26px;line-height:54px;text-align:center;opacity:.8}.ant-upload-list-picture-card .ant-upload-list-item-icon,.ant-upload-list-picture .ant-upload-list-item-icon{position:absolute;top:50%;left:50%;font-size:26px;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%)}.ant-upload-list-picture-card .ant-upload-list-item-image,.ant-upload-list-picture .ant-upload-list-item-image{max-width:100%}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail img,.ant-upload-list-picture .ant-upload-list-item-thumbnail img{display:block;width:48px;height:48px;overflow:hidden}.ant-upload-list-picture-card .ant-upload-list-item-name,.ant-upload-list-picture .ant-upload-list-item-name{display:inline-block;-webkit-box-sizing:border-box;box-sizing:border-box;max-width:100%;margin:0 0 0 8px;padding-right:8px;padding-left:48px;overflow:hidden;line-height:44px;white-space:nowrap;text-overflow:ellipsis;-webkit-transition:all .3s;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-name-icon-count-1,.ant-upload-list-picture .ant-upload-list-item-name-icon-count-1{padding-right:18px}.ant-upload-list-picture-card .ant-upload-list-item-name-icon-count-2,.ant-upload-list-picture .ant-upload-list-item-name-icon-count-2{padding-right:36px}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-name,.ant-upload-list-picture .ant-upload-list-item-uploading .ant-upload-list-item-name{line-height:28px}.ant-upload-list-picture-card .ant-upload-list-item-progress,.ant-upload-list-picture .ant-upload-list-item-progress{bottom:14px;width:calc(100% - 24px);margin-top:0;padding-left:56px}.ant-upload-list-picture-card .anticon-close,.ant-upload-list-picture .anticon-close{position:absolute;top:8px;right:8px;line-height:1;opacity:1}.ant-upload-list-picture-card.ant-upload-list:after{display:none}.ant-upload-list-picture-card-container,.ant-upload-list-picture-card .ant-upload-list-item{float:left;width:104px;height:104px;margin:0 8px 8px 0}.ant-upload-list-picture-card .ant-upload-list-item-info{position:relative;height:100%;overflow:hidden}.ant-upload-list-picture-card .ant-upload-list-item-info:before{position:absolute;z-index:1;width:100%;height:100%;background-color:rgba(0,0,0,.5);opacity:0;-webkit-transition:all .3s;transition:all .3s;content:" "}.ant-upload-list-picture-card .ant-upload-list-item:hover .ant-upload-list-item-info:before{opacity:1}.ant-upload-list-picture-card .ant-upload-list-item-actions{position:absolute;top:50%;left:50%;z-index:10;white-space:nowrap;-webkit-transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%);transform:translate(-50%,-50%);opacity:0;-webkit-transition:all .3s;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-delete,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-download,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-eye-o{z-index:10;width:16px;margin:0 4px;color:hsla(0,0%,100%,.85);font-size:16px;cursor:pointer;-webkit-transition:all .3s;transition:all .3s}.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-delete:hover,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-download:hover,.ant-upload-list-picture-card .ant-upload-list-item-actions .anticon-eye-o:hover{color:#fff}.ant-upload-list-picture-card .ant-upload-list-item-actions:hover,.ant-upload-list-picture-card .ant-upload-list-item-info:hover+.ant-upload-list-item-actions{opacity:1}.ant-upload-list-picture-card .ant-upload-list-item-thumbnail,.ant-upload-list-picture-card .ant-upload-list-item-thumbnail img{position:static;display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}.ant-upload-list-picture-card .ant-upload-list-item-name{display:none;margin:8px 0 0;padding:0;line-height:1.5;text-align:center}.ant-upload-list-picture-card .anticon-picture+.ant-upload-list-item-name{position:absolute;bottom:10px;display:block}.ant-upload-list-picture-card .ant-upload-list-item-uploading.ant-upload-list-item{background-color:#fafafa}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info{height:auto}.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info .anticon-delete,.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info .anticon-eye-o,.ant-upload-list-picture-card .ant-upload-list-item-uploading .ant-upload-list-item-info:before{display:none}.ant-upload-list-picture-card .ant-upload-list-item-uploading-text{margin-top:18px;color:rgba(0,0,0,.45)}.ant-upload-list-picture-card .ant-upload-list-item-progress{bottom:32px;padding-left:0}.ant-upload-list .ant-upload-success-icon{color:#52c41a;font-weight:700}.ant-upload-list .ant-upload-animate-enter,.ant-upload-list .ant-upload-animate-inline-enter,.ant-upload-list .ant-upload-animate-inline-leave,.ant-upload-list .ant-upload-animate-leave{-webkit-animation-duration:.3s;animation-duration:.3s;-webkit-animation-fill-mode:cubic-bezier(.78,.14,.15,.86);animation-fill-mode:cubic-bezier(.78,.14,.15,.86)}.ant-upload-list .ant-upload-animate-enter{-webkit-animation-name:uploadAnimateIn;animation-name:uploadAnimateIn}.ant-upload-list .ant-upload-animate-leave{-webkit-animation-name:uploadAnimateOut;animation-name:uploadAnimateOut}.ant-upload-list .ant-upload-animate-inline-enter{-webkit-animation-name:uploadAnimateInlineIn;animation-name:uploadAnimateInlineIn}.ant-upload-list .ant-upload-animate-inline-leave{-webkit-animation-name:uploadAnimateInlineOut;animation-name:uploadAnimateInlineOut}@-webkit-keyframes uploadAnimateIn{0%{height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateIn{0%{height:0;margin:0;padding:0;opacity:0}}@-webkit-keyframes uploadAnimateOut{to{height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateOut{to{height:0;margin:0;padding:0;opacity:0}}@-webkit-keyframes uploadAnimateInlineIn{0%{width:0;height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateInlineIn{0%{width:0;height:0;margin:0;padding:0;opacity:0}}@-webkit-keyframes uploadAnimateInlineOut{to{width:0;height:0;margin:0;padding:0;opacity:0}}@keyframes uploadAnimateInlineOut{to{width:0;height:0;margin:0;padding:0;opacity:0}} /*# sourceMappingURL=2.8ca66de9.chunk.css.map */ ================================================ FILE: package_hub/template/inspection_html/static/css/main.041ca26a.chunk.css ================================================ .warningSearch{display:flex;justify-content:flex-end;margin-top:10px;margin-bottom:10px}.warningSearch>div:first-child{margin-right:auto}.rangePicker{margin-right:15px}.contentLeftMenuWrapper{display:flex}.leftMenuListContent{display:flex;flex-direction:column;font-size:16px}.leftMenu{background-color:#fafafa;padding:15px;margin-top:10px;margin-right:10px;height:82vh;overflow-y:auto}.trendContentWrapper{width:100%;padding:10px}.title{margin-bottom:5px;margin-left:15px;color:#333;font-size:16px;font-weight:500;padding:10px 0}.trendItemContent{background-color:#fff;border-radius:10px}.rangPicker{display:flex;justify-content:flex-end;width:100%;padding:10px 60px}.header{display:flex;align-items:center;padding:8px 8px 8px 15px;color:#333;border-bottom:1px solid #bfbfbf}.header>div:nth-child(2){display:flex;align-items:center;width:100%;height:35px;margin-left:5px}.pageInfo{display:flex;justify-content:flex-end;align-items:center;padding:10px}.pageInfo>div:first-child{margin-right:10px}.panelItem{background:#edf0f3;border-radius:4px;border:0;overflow:hidden;border-bottom:0!important}.panelItem>div:first-child{padding:8px 15px}.panelItem>div:nth-child(2){background-color:#fff!important}.reportContent{padding:15px}.reportTitle{display:flex;justify-content:center;color:#1890ff;margin-bottom:20px;height:40px;align-items:center;position:relative}.reportTitle>div:first-child{font-size:22px;font-weight:500;color:#333}.reportTitle>div:nth-child(2){position:absolute;right:0;display:flex;cursor:pointer;margin-right:10px}.overviewItemWrapper{display:flex;justify-content:space-between;flex-flow:wrap;margin:10px 0}.overviewItemWrapper>div:nth-child(2n){margin-right:0}.overviewItem{display:flex;width:49.5%;color:#333;margin-bottom:10px}.overviewItem>div{border:1px solid #e8e8e8;padding:10px}.overviewItem>div:first-child{display:flex;justify-content:center;align-items:center;border-right:none;width:200px}.overviewItem>div:nth-child(2){width:100%}.planChartWrapper{margin-top:10px;width:100%;border:1px solid #e8e8e8;border-radius:2px}.planChartBlockWrapper{display:flex;flex-flow:row wrap;max-height:240px;overflow-y:auto;padding:20px}.stateButton{position:relative;top:0;border:1px solid #333;transition:all .2s ease-in-out;width:178px;margin-right:32px;margin-bottom:10px;height:32px}.stateButton>div{margin:auto;width:100%;height:100%;line-height:30px;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.planChartTitle{background-color:#fafbfd;font-weight:500;height:30px;line-height:30px;border-bottom:1px solid #e8e8e8}.planChartTitleCircular{display:inline-block;width:10px;height:10px;background-color:#54bba6;border-radius:50%;margin-right:10px;margin-left:20px}.topologyWrapper{display:flex;align-items:center;margin-right:80px;margin-bottom:80px}.topologyChildren{display:flex;flex-direction:column}.topologyChildren>div:first-child{position:relative}.verticalLine{position:absolute;background-color:#333;left:0;top:22px;height:calc(100% - 55px);width:2px;border-radius:1px}.topologyItem{justify-content:center;padding:10px;border:2px solid #333;border-radius:5px}.rootItemBox,.topologyItem{display:flex;align-items:center}.rootItemBox{margin-bottom:10px}.connectLine{display:inline-block;width:30px;background-color:#333;height:2px;border-radius:1px}.basicCardWrapper{display:flex;flex-flow:row wrap}.basicCardItem{width:40%;padding:5px;margin-right:10px;margin-bottom:10px}.buttonContainer{width:100%}.greenType{background-color:#5ba165;color:#fff;border-color:#5ba165}.redType{background-color:#ff4d4f;color:#fff;border-color:#ff4d4f}.buttonContainer>button{margin-right:15px}._bigfontSize{font-size:14px} /*# sourceMappingURL=main.041ca26a.chunk.css.map */ ================================================ FILE: package_hub/template/inspection_html/static/js/2.0ca9bd94.chunk.js ================================================ /*! For license information please see 2.0ca9bd94.chunk.js.LICENSE.txt */ (this["webpackJsonpomp-fontend"]=this["webpackJsonpomp-fontend"]||[]).push([[2],[function(e,t,n){"use strict";e.exports=n(168)},function(e,t,n){e.exports=n(211)()},function(e,t,n){"use strict";e.exports=n(173)},function(e,t,n){var r;!function(){"use strict";var n={}.hasOwnProperty;function o(){for(var e=[],t=0;t1&&void 0!==arguments[1]?arguments[1]:O;if(e){var n=this.definitions.get(e);return n&&"function"===typeof n.icon&&(n=s()({},n,{icon:n.icon(t.primaryColor,t.secondaryColor)})),n}}},{key:"setTwoToneColors",value:function(e){var t=e.primaryColor,n=e.secondaryColor;O.primaryColor=t,O.secondaryColor=n||Object(z.c)(t)}},{key:"getTwoToneColors",value:function(){return s()({},O)}}]),t}(r.Component);C.displayName="IconReact",C.definitions=new z.a;var M=C;function S(){return(S=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:{},t=e.scriptUrl,n=e.extraCommonProps,o=void 0===n?{}:n;if("undefined"!==typeof document&&"undefined"!==typeof window&&"function"===typeof document.createElement&&"string"===typeof t&&t.length&&!x.has(t)){var c=document.createElement("script");c.setAttribute("src",t),c.setAttribute("data-namespace",t),x.add(t),document.body.appendChild(c)}var i=function(e){var t=e.type,n=e.children,c=_(e,["type","children"]),i=null;return e.type&&(i=r.createElement("use",{xlinkHref:"#".concat(t)})),n&&(i=n),r.createElement(U,S({},o,c),i)};return i.displayName="Iconfont",i},W.getTwoToneColor=function(){return M.getTwoToneColors().primaryColor},W.setTwoToneColor=L;var U=t.a=W},function(e,t,n){"use strict";t.__esModule=!0;var r,o=n(133),c=(r=o)&&r.__esModule?r:{default:r};t.default=function(){function e(e,t){for(var n=0;n=r.F1&&t<=r.F12)return!1;switch(t){case r.ALT:case r.CAPS_LOCK:case r.CONTEXT_MENU:case r.CTRL:case r.DOWN:case r.END:case r.ESC:case r.HOME:case r.INSERT:case r.LEFT:case r.MAC_FF_META:case r.META:case r.NUMLOCK:case r.NUM_CENTER:case r.PAGE_DOWN:case r.PAGE_UP:case r.PAUSE:case r.PRINT_SCREEN:case r.RIGHT:case r.SHIFT:case r.UP:case r.WIN_KEY:case r.WIN_KEY_RIGHT:return!1;default:return!0}},isCharacterKey:function(e){if(e>=r.ZERO&&e<=r.NINE)return!0;if(e>=r.NUM_ZERO&&e<=r.NUM_MULTIPLY)return!0;if(e>=r.A&&e<=r.Z)return!0;if(-1!==window.navigator.userAgent.indexOf("WebKit")&&0===e)return!0;switch(e){case r.SPACE:case r.QUESTION_MARK:case r.NUM_PLUS:case r.NUM_MINUS:case r.NUM_PERIOD:case r.NUM_DIVISION:case r.SEMICOLON:case r.DASH:case r.EQUALS:case r.COMMA:case r.PERIOD:case r.SLASH:case r.APOSTROPHE:case r.SINGLE_QUOTE:case r.OPEN_SQUARE_BRACKET:case r.BACKSLASH:case r.CLOSE_SQUARE_BRACKET:return!0;default:return!1}}};t.a=r},function(e,t,n){(function(t){for(var r=n(223),o="undefined"===typeof window?t:window,c=["moz","webkit"],i="AnimationFrame",a=o["request"+i],l=o["cancel"+i]||o["cancelRequest"+i],u=0;!a&&u>>0;for(t=0;t0)for(n=0;n=0?n?"+":"":"-")+Math.pow(10,Math.max(0,o)).toString().substr(1)+r}var N=/(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|N{1,5}|YYYYYY|YYYYY|YYYY|YY|y{2,4}|yo?|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|kk?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g,D=/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g,R={},A={};function I(e,t,n,r){var o=r;"string"===typeof r&&(o=function(){return this[r]()}),e&&(A[e]=o),t&&(A[t[0]]=function(){return j(o.apply(this,arguments),t[1],t[2])}),n&&(A[n]=function(){return this.localeData().ordinal(o.apply(this,arguments),e)})}function F(e){return e.match(/\[[\s\S]/)?e.replace(/^\[|\]$/g,""):e.replace(/\\/g,"")}function W(e){var t,n,r=e.match(N);for(t=0,n=r.length;t=0&&D.test(e);)e=e.replace(D,r),D.lastIndex=0,n-=1;return e}var B={LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"};function Y(e){var t=this._longDateFormat[e],n=this._longDateFormat[e.toUpperCase()];return t||!n?t:(this._longDateFormat[e]=n.match(N).map((function(e){return"MMMM"===e||"MM"===e||"DD"===e||"dddd"===e?e.slice(1):e})).join(""),this._longDateFormat[e])}var q="Invalid date";function G(){return this._invalidDate}var $="%d",Q=/\d{1,2}/;function X(e){return this._ordinal.replace("%d",e)}var Z={future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",w:"a week",ww:"%d weeks",M:"a month",MM:"%d months",y:"a year",yy:"%d years"};function J(e,t,n,r){var o=this._relativeTime[n];return H(o)?o(e,t,n,r):o.replace(/%d/i,e)}function ee(e,t){var n=this._relativeTime[e>0?"future":"past"];return H(n)?n(t):n.replace(/%s/i,t)}var te={};function ne(e,t){var n=e.toLowerCase();te[n]=te[n+"s"]=te[t]=e}function re(e){return"string"===typeof e?te[e]||te[e.toLowerCase()]:void 0}function oe(e){var t,n,r={};for(n in e)a(e,n)&&(t=re(n))&&(r[t]=e[n]);return r}var ce={};function ie(e,t){ce[e]=t}function ae(e){var t,n=[];for(t in e)a(e,t)&&n.push({unit:t,priority:ce[t]});return n.sort((function(e,t){return e.priority-t.priority})),n}function le(e){return e%4===0&&e%100!==0||e%400===0}function ue(e){return e<0?Math.ceil(e)||0:Math.floor(e)}function se(e){var t=+e,n=0;return 0!==t&&isFinite(t)&&(n=ue(t)),n}function fe(e,t){return function(n){return null!=n?(he(this,e,n),r.updateOffset(this,t),this):pe(this,e)}}function pe(e,t){return e.isValid()?e._d["get"+(e._isUTC?"UTC":"")+t]():NaN}function he(e,t,n){e.isValid()&&!isNaN(n)&&("FullYear"===t&&le(e.year())&&1===e.month()&&29===e.date()?(n=se(n),e._d["set"+(e._isUTC?"UTC":"")+t](n,e.month(),Je(n,e.month()))):e._d["set"+(e._isUTC?"UTC":"")+t](n))}function de(e){return H(this[e=re(e)])?this[e]():this}function ve(e,t){if("object"===typeof e){var n,r=ae(e=oe(e));for(n=0;n68?1900:2e3)};var mt=fe("FullYear",!0);function yt(){return le(this.year())}function bt(e,t,n,r,o,c,i){var a;return e<100&&e>=0?(a=new Date(e+400,t,n,r,o,c,i),isFinite(a.getFullYear())&&a.setFullYear(e)):a=new Date(e,t,n,r,o,c,i),a}function gt(e){var t,n;return e<100&&e>=0?((n=Array.prototype.slice.call(arguments))[0]=e+400,t=new Date(Date.UTC.apply(null,n)),isFinite(t.getUTCFullYear())&&t.setUTCFullYear(e)):t=new Date(Date.UTC.apply(null,arguments)),t}function wt(e,t,n){var r=7+t-n;return-(7+gt(e,0,r).getUTCDay()-t)%7+r-1}function zt(e,t,n,r,o){var c,i,a=1+7*(t-1)+(7+n-r)%7+wt(e,r,o);return a<=0?i=vt(c=e-1)+a:a>vt(e)?(c=e+1,i=a-vt(e)):(c=e,i=a),{year:c,dayOfYear:i}}function Ot(e,t,n){var r,o,c=wt(e.year(),t,n),i=Math.floor((e.dayOfYear()-c-1)/7)+1;return i<1?r=i+Ct(o=e.year()-1,t,n):i>Ct(e.year(),t,n)?(r=i-Ct(e.year(),t,n),o=e.year()+1):(o=e.year(),r=i),{week:r,year:o}}function Ct(e,t,n){var r=wt(e,t,n),o=wt(e+1,t,n);return(vt(e)-r+o)/7}function Mt(e){return Ot(e,this._week.dow,this._week.doy).week}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),ne("week","w"),ne("isoWeek","W"),ie("week",5),ie("isoWeek",5),Le("w",Oe),Le("ww",Oe,be),Le("W",Oe),Le("WW",Oe,be),Ie(["w","ww","W","WW"],(function(e,t,n,r){t[r.substr(0,1)]=se(e)}));var St={dow:0,doy:6};function _t(){return this._week.dow}function xt(){return this._week.doy}function kt(e){var t=this.localeData().week(this);return null==e?t:this.add(7*(e-t),"d")}function Ht(e){var t=Ot(this,1,4).week;return null==e?t:this.add(7*(e-t),"d")}function Et(e,t){return"string"!==typeof e?e:isNaN(e)?"number"===typeof(e=t.weekdaysParse(e))?e:null:parseInt(e,10)}function Pt(e,t){return"string"===typeof e?t.weekdaysParse(e)%7||7:isNaN(e)?null:e}function Vt(e,t){return e.slice(t,7).concat(e.slice(0,t))}I("d",0,"do","day"),I("dd",0,0,(function(e){return this.localeData().weekdaysMin(this,e)})),I("ddd",0,0,(function(e){return this.localeData().weekdaysShort(this,e)})),I("dddd",0,0,(function(e){return this.localeData().weekdays(this,e)})),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),ne("day","d"),ne("weekday","e"),ne("isoWeekday","E"),ie("day",11),ie("weekday",11),ie("isoWeekday",11),Le("d",Oe),Le("e",Oe),Le("E",Oe),Le("dd",(function(e,t){return t.weekdaysMinRegex(e)})),Le("ddd",(function(e,t){return t.weekdaysShortRegex(e)})),Le("dddd",(function(e,t){return t.weekdaysRegex(e)})),Ie(["dd","ddd","dddd"],(function(e,t,n,r){var o=n._locale.weekdaysParse(e,r,n._strict);null!=o?t.d=o:m(n).invalidWeekday=e})),Ie(["d","e","E"],(function(e,t,n,r){t[r]=se(e)}));var Tt="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),Lt="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"),jt="Su_Mo_Tu_We_Th_Fr_Sa".split("_"),Nt=Te,Dt=Te,Rt=Te;function At(e,t){var n=c(this._weekdays)?this._weekdays:this._weekdays[e&&!0!==e&&this._weekdays.isFormat.test(t)?"format":"standalone"];return!0===e?Vt(n,this._week.dow):e?n[e.day()]:n}function It(e){return!0===e?Vt(this._weekdaysShort,this._week.dow):e?this._weekdaysShort[e.day()]:this._weekdaysShort}function Ft(e){return!0===e?Vt(this._weekdaysMin,this._week.dow):e?this._weekdaysMin[e.day()]:this._weekdaysMin}function Wt(e,t,n){var r,o,c,i=e.toLocaleLowerCase();if(!this._weekdaysParse)for(this._weekdaysParse=[],this._shortWeekdaysParse=[],this._minWeekdaysParse=[],r=0;r<7;++r)c=d([2e3,1]).day(r),this._minWeekdaysParse[r]=this.weekdaysMin(c,"").toLocaleLowerCase(),this._shortWeekdaysParse[r]=this.weekdaysShort(c,"").toLocaleLowerCase(),this._weekdaysParse[r]=this.weekdays(c,"").toLocaleLowerCase();return n?"dddd"===t?-1!==(o=We.call(this._weekdaysParse,i))?o:null:"ddd"===t?-1!==(o=We.call(this._shortWeekdaysParse,i))?o:null:-1!==(o=We.call(this._minWeekdaysParse,i))?o:null:"dddd"===t?-1!==(o=We.call(this._weekdaysParse,i))||-1!==(o=We.call(this._shortWeekdaysParse,i))||-1!==(o=We.call(this._minWeekdaysParse,i))?o:null:"ddd"===t?-1!==(o=We.call(this._shortWeekdaysParse,i))||-1!==(o=We.call(this._weekdaysParse,i))||-1!==(o=We.call(this._minWeekdaysParse,i))?o:null:-1!==(o=We.call(this._minWeekdaysParse,i))||-1!==(o=We.call(this._weekdaysParse,i))||-1!==(o=We.call(this._shortWeekdaysParse,i))?o:null}function Ut(e,t,n){var r,o,c;if(this._weekdaysParseExact)return Wt.call(this,e,t,n);for(this._weekdaysParse||(this._weekdaysParse=[],this._minWeekdaysParse=[],this._shortWeekdaysParse=[],this._fullWeekdaysParse=[]),r=0;r<7;r++){if(o=d([2e3,1]).day(r),n&&!this._fullWeekdaysParse[r]&&(this._fullWeekdaysParse[r]=new RegExp("^"+this.weekdays(o,"").replace(".","\\.?")+"$","i"),this._shortWeekdaysParse[r]=new RegExp("^"+this.weekdaysShort(o,"").replace(".","\\.?")+"$","i"),this._minWeekdaysParse[r]=new RegExp("^"+this.weekdaysMin(o,"").replace(".","\\.?")+"$","i")),this._weekdaysParse[r]||(c="^"+this.weekdays(o,"")+"|^"+this.weekdaysShort(o,"")+"|^"+this.weekdaysMin(o,""),this._weekdaysParse[r]=new RegExp(c.replace(".",""),"i")),n&&"dddd"===t&&this._fullWeekdaysParse[r].test(e))return r;if(n&&"ddd"===t&&this._shortWeekdaysParse[r].test(e))return r;if(n&&"dd"===t&&this._minWeekdaysParse[r].test(e))return r;if(!n&&this._weekdaysParse[r].test(e))return r}}function Kt(e){if(!this.isValid())return null!=e?this:NaN;var t=this._isUTC?this._d.getUTCDay():this._d.getDay();return null!=e?(e=Et(e,this.localeData()),this.add(e-t,"d")):t}function Bt(e){if(!this.isValid())return null!=e?this:NaN;var t=(this.day()+7-this.localeData()._week.dow)%7;return null==e?t:this.add(e-t,"d")}function Yt(e){if(!this.isValid())return null!=e?this:NaN;if(null!=e){var t=Pt(e,this.localeData());return this.day(this.day()%7?t:t-7)}return this.day()||7}function qt(e){return this._weekdaysParseExact?(a(this,"_weekdaysRegex")||Qt.call(this),e?this._weekdaysStrictRegex:this._weekdaysRegex):(a(this,"_weekdaysRegex")||(this._weekdaysRegex=Nt),this._weekdaysStrictRegex&&e?this._weekdaysStrictRegex:this._weekdaysRegex)}function Gt(e){return this._weekdaysParseExact?(a(this,"_weekdaysRegex")||Qt.call(this),e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex):(a(this,"_weekdaysShortRegex")||(this._weekdaysShortRegex=Dt),this._weekdaysShortStrictRegex&&e?this._weekdaysShortStrictRegex:this._weekdaysShortRegex)}function $t(e){return this._weekdaysParseExact?(a(this,"_weekdaysRegex")||Qt.call(this),e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex):(a(this,"_weekdaysMinRegex")||(this._weekdaysMinRegex=Rt),this._weekdaysMinStrictRegex&&e?this._weekdaysMinStrictRegex:this._weekdaysMinRegex)}function Qt(){function e(e,t){return t.length-e.length}var t,n,r,o,c,i=[],a=[],l=[],u=[];for(t=0;t<7;t++)n=d([2e3,1]).day(t),r=De(this.weekdaysMin(n,"")),o=De(this.weekdaysShort(n,"")),c=De(this.weekdays(n,"")),i.push(r),a.push(o),l.push(c),u.push(r),u.push(o),u.push(c);i.sort(e),a.sort(e),l.sort(e),u.sort(e),this._weekdaysRegex=new RegExp("^("+u.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+l.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+a.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+i.join("|")+")","i")}function Xt(){return this.hours()%12||12}function Zt(){return this.hours()||24}function Jt(e,t){I(e,0,0,(function(){return this.localeData().meridiem(this.hours(),this.minutes(),t)}))}function en(e,t){return t._meridiemParse}function tn(e){return"p"===(e+"").toLowerCase().charAt(0)}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,Xt),I("k",["kk",2],0,Zt),I("hmm",0,0,(function(){return""+Xt.apply(this)+j(this.minutes(),2)})),I("hmmss",0,0,(function(){return""+Xt.apply(this)+j(this.minutes(),2)+j(this.seconds(),2)})),I("Hmm",0,0,(function(){return""+this.hours()+j(this.minutes(),2)})),I("Hmmss",0,0,(function(){return""+this.hours()+j(this.minutes(),2)+j(this.seconds(),2)})),Jt("a",!0),Jt("A",!1),ne("hour","h"),ie("hour",13),Le("a",en),Le("A",en),Le("H",Oe),Le("h",Oe),Le("k",Oe),Le("HH",Oe,be),Le("hh",Oe,be),Le("kk",Oe,be),Le("hmm",Ce),Le("hmmss",Me),Le("Hmm",Ce),Le("Hmmss",Me),Ae(["H","HH"],Ye),Ae(["k","kk"],(function(e,t,n){var r=se(e);t[Ye]=24===r?0:r})),Ae(["a","A"],(function(e,t,n){n._isPm=n._locale.isPM(e),n._meridiem=e})),Ae(["h","hh"],(function(e,t,n){t[Ye]=se(e),m(n).bigHour=!0})),Ae("hmm",(function(e,t,n){var r=e.length-2;t[Ye]=se(e.substr(0,r)),t[qe]=se(e.substr(r)),m(n).bigHour=!0})),Ae("hmmss",(function(e,t,n){var r=e.length-4,o=e.length-2;t[Ye]=se(e.substr(0,r)),t[qe]=se(e.substr(r,2)),t[Ge]=se(e.substr(o)),m(n).bigHour=!0})),Ae("Hmm",(function(e,t,n){var r=e.length-2;t[Ye]=se(e.substr(0,r)),t[qe]=se(e.substr(r))})),Ae("Hmmss",(function(e,t,n){var r=e.length-4,o=e.length-2;t[Ye]=se(e.substr(0,r)),t[qe]=se(e.substr(r,2)),t[Ge]=se(e.substr(o))}));var nn=/[ap]\.?m?\.?/i,rn=fe("Hours",!0);function on(e,t,n){return e>11?n?"pm":"PM":n?"am":"AM"}var cn,an={calendar:T,longDateFormat:B,invalidDate:q,ordinal:$,dayOfMonthOrdinalParse:Q,relativeTime:Z,months:et,monthsShort:tt,week:St,weekdays:Tt,weekdaysMin:jt,weekdaysShort:Lt,meridiemParse:nn},ln={},un={};function sn(e,t){var n,r=Math.min(e.length,t.length);for(n=0;n0;){if(r=hn(o.slice(0,t).join("-")))return r;if(n&&n.length>=t&&sn(o,n)>=t-1)break;t--}c++}return cn}function hn(t){var n=null;if(void 0===ln[t]&&"undefined"!==typeof e&&e&&e.exports)try{n=cn._abbr,function(){var e=new Error("Cannot find module 'undefined'");throw e.code="MODULE_NOT_FOUND",e}(),dn(n)}catch(r){ln[t]=null}return ln[t]}function dn(e,t){var n;return e&&((n=u(t)?yn(e):vn(e,t))?cn=n:"undefined"!==typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),cn._abbr}function vn(e,t){if(null!==t){var n,r=an;if(t.abbr=e,null!=ln[e])k("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),r=ln[e]._config;else if(null!=t.parentLocale)if(null!=ln[t.parentLocale])r=ln[t.parentLocale]._config;else{if(null==(n=hn(t.parentLocale)))return un[t.parentLocale]||(un[t.parentLocale]=[]),un[t.parentLocale].push({name:e,config:t}),null;r=n._config}return ln[e]=new V(P(r,t)),un[e]&&un[e].forEach((function(e){vn(e.name,e.config)})),dn(e),ln[e]}return delete ln[e],null}function mn(e,t){if(null!=t){var n,r,o=an;null!=ln[e]&&null!=ln[e].parentLocale?ln[e].set(P(ln[e]._config,t)):(null!=(r=hn(e))&&(o=r._config),t=P(o,t),null==r&&(t.abbr=e),(n=new V(t)).parentLocale=ln[e],ln[e]=n),dn(e)}else null!=ln[e]&&(null!=ln[e].parentLocale?(ln[e]=ln[e].parentLocale,e===dn()&&dn(e)):null!=ln[e]&&delete ln[e]);return ln[e]}function yn(e){var t;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return cn;if(!c(e)){if(t=hn(e))return t;e=[e]}return pn(e)}function bn(){return _(ln)}function gn(e){var t,n=e._a;return n&&-2===m(e).overflow&&(t=n[Ke]<0||n[Ke]>11?Ke:n[Be]<1||n[Be]>Je(n[Ue],n[Ke])?Be:n[Ye]<0||n[Ye]>24||24===n[Ye]&&(0!==n[qe]||0!==n[Ge]||0!==n[$e])?Ye:n[qe]<0||n[qe]>59?qe:n[Ge]<0||n[Ge]>59?Ge:n[$e]<0||n[$e]>999?$e:-1,m(e)._overflowDayOfYear&&(tBe)&&(t=Be),m(e)._overflowWeeks&&-1===t&&(t=Qe),m(e)._overflowWeekday&&-1===t&&(t=Xe),m(e).overflow=t),e}var wn=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,zn=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d|))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([+-]\d\d(?::?\d\d)?|\s*Z)?)?$/,On=/Z|[+-]\d\d(?::?\d\d)?/,Cn=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/],["YYYYMM",/\d{6}/,!1],["YYYY",/\d{4}/,!1]],Mn=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],Sn=/^\/?Date\((-?\d+)/i,_n=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),?\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|([+-]\d{4}))$/,xn={UT:0,GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function kn(e){var t,n,r,o,c,i,a=e._i,l=wn.exec(a)||zn.exec(a);if(l){for(m(e).iso=!0,t=0,n=Cn.length;tvt(c)||0===e._dayOfYear)&&(m(e)._overflowDayOfYear=!0),n=gt(c,0,e._dayOfYear),e._a[Ke]=n.getUTCMonth(),e._a[Be]=n.getUTCDate()),t=0;t<3&&null==e._a[t];++t)e._a[t]=i[t]=r[t];for(;t<7;t++)e._a[t]=i[t]=null==e._a[t]?2===t?1:0:e._a[t];24===e._a[Ye]&&0===e._a[qe]&&0===e._a[Ge]&&0===e._a[$e]&&(e._nextDay=!0,e._a[Ye]=0),e._d=(e._useUTC?gt:bt).apply(null,i),o=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Ye]=24),e._w&&"undefined"!==typeof e._w.d&&e._w.d!==o&&(m(e).weekdayMismatch=!0)}}function An(e){var t,n,r,o,c,i,a,l,u;null!=(t=e._w).GG||null!=t.W||null!=t.E?(c=1,i=4,n=Nn(t.GG,e._a[Ue],Ot(Gn(),1,4).year),r=Nn(t.W,1),((o=Nn(t.E,1))<1||o>7)&&(l=!0)):(c=e._locale._week.dow,i=e._locale._week.doy,u=Ot(Gn(),c,i),n=Nn(t.gg,e._a[Ue],u.year),r=Nn(t.w,u.week),null!=t.d?((o=t.d)<0||o>6)&&(l=!0):null!=t.e?(o=t.e+c,(t.e<0||t.e>6)&&(l=!0)):o=c),r<1||r>Ct(n,c,i)?m(e)._overflowWeeks=!0:null!=l?m(e)._overflowWeekday=!0:(a=zt(n,r,o,c,i),e._a[Ue]=a.year,e._dayOfYear=a.dayOfYear)}function In(e){if(e._f!==r.ISO_8601)if(e._f!==r.RFC_2822){e._a=[],m(e).empty=!0;var t,n,o,c,i,a,l=""+e._i,u=l.length,s=0;for(o=K(e._f,e._locale).match(N)||[],t=0;t0&&m(e).unusedInput.push(i),l=l.slice(l.indexOf(n)+n.length),s+=n.length),A[c]?(n?m(e).empty=!1:m(e).unusedTokens.push(c),Fe(c,n,e)):e._strict&&!n&&m(e).unusedTokens.push(c);m(e).charsLeftOver=u-s,l.length>0&&m(e).unusedInput.push(l),e._a[Ye]<=12&&!0===m(e).bigHour&&e._a[Ye]>0&&(m(e).bigHour=void 0),m(e).parsedDateParts=e._a.slice(0),m(e).meridiem=e._meridiem,e._a[Ye]=Fn(e._locale,e._a[Ye],e._meridiem),null!==(a=m(e).era)&&(e._a[Ue]=e._locale.erasConvertYear(a,e._a[Ue])),Rn(e),gn(e)}else Ln(e);else kn(e)}function Fn(e,t,n){var r;return null==n?t:null!=e.meridiemHour?e.meridiemHour(t,n):null!=e.isPM?((r=e.isPM(n))&&t<12&&(t+=12),r||12!==t||(t=0),t):t}function Wn(e){var t,n,r,o,c,i,a=!1;if(0===e._f.length)return m(e).invalidFormat=!0,void(e._d=new Date(NaN));for(o=0;othis?this:e:b()}));function Xn(e,t){var n,r;if(1===t.length&&c(t[0])&&(t=t[0]),!t.length)return Gn();for(n=t[0],r=1;rthis.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()}function zr(){if(!u(this._isDSTShifted))return this._isDSTShifted;var e,t={};return z(t,this),(t=Bn(t))._a?(e=t._isUTC?d(t._a):Gn(t._a),this._isDSTShifted=this.isValid()&&lr(t._a,e.toArray())>0):this._isDSTShifted=!1,this._isDSTShifted}function Or(){return!!this.isValid()&&!this._isUTC}function Cr(){return!!this.isValid()&&this._isUTC}function Mr(){return!!this.isValid()&&this._isUTC&&0===this._offset}r.updateOffset=function(){};var Sr=/^(-|\+)?(?:(\d*)[. ])?(\d+):(\d+)(?::(\d+)(\.\d*)?)?$/,_r=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/;function xr(e,t){var n,r,o,c=e,i=null;return ir(e)?c={ms:e._milliseconds,d:e._days,M:e._months}:s(e)||!isNaN(+e)?(c={},t?c[t]=+e:c.milliseconds=+e):(i=Sr.exec(e))?(n="-"===i[1]?-1:1,c={y:0,d:se(i[Be])*n,h:se(i[Ye])*n,m:se(i[qe])*n,s:se(i[Ge])*n,ms:se(ar(1e3*i[$e]))*n}):(i=_r.exec(e))?(n="-"===i[1]?-1:1,c={y:kr(i[2],n),M:kr(i[3],n),w:kr(i[4],n),d:kr(i[5],n),h:kr(i[6],n),m:kr(i[7],n),s:kr(i[8],n)}):null==c?c={}:"object"===typeof c&&("from"in c||"to"in c)&&(o=Er(Gn(c.from),Gn(c.to)),(c={}).ms=o.milliseconds,c.M=o.months),r=new cr(c),ir(e)&&a(e,"_locale")&&(r._locale=e._locale),ir(e)&&a(e,"_isValid")&&(r._isValid=e._isValid),r}function kr(e,t){var n=e&&parseFloat(e.replace(",","."));return(isNaN(n)?0:n)*t}function Hr(e,t){var n={};return n.months=t.month()-e.month()+12*(t.year()-e.year()),e.clone().add(n.months,"M").isAfter(t)&&--n.months,n.milliseconds=+t-+e.clone().add(n.months,"M"),n}function Er(e,t){var n;return e.isValid()&&t.isValid()?(t=pr(t,e),e.isBefore(t)?n=Hr(e,t):((n=Hr(t,e)).milliseconds=-n.milliseconds,n.months=-n.months),n):{milliseconds:0,months:0}}function Pr(e,t){return function(n,r){var o;return null===r||isNaN(+r)||(k(t,"moment()."+t+"(period, number) is deprecated. Please use moment()."+t+"(number, period). See http://momentjs.com/guides/#/warnings/add-inverted-param/ for more info."),o=n,n=r,r=o),Vr(this,xr(n,r),e),this}}function Vr(e,t,n,o){var c=t._milliseconds,i=ar(t._days),a=ar(t._months);e.isValid()&&(o=null==o||o,a&&ut(e,pe(e,"Month")+a*n),i&&he(e,"Date",pe(e,"Date")+i*n),c&&e._d.setTime(e._d.valueOf()+c*n),o&&r.updateOffset(e,i||a))}xr.fn=cr.prototype,xr.invalid=or;var Tr=Pr(1,"add"),Lr=Pr(-1,"subtract");function jr(e){return"string"===typeof e||e instanceof String}function Nr(e){return C(e)||f(e)||jr(e)||s(e)||Rr(e)||Dr(e)||null===e||void 0===e}function Dr(e){var t,n,r=i(e)&&!l(e),o=!1,c=["years","year","y","months","month","M","days","day","d","dates","date","D","hours","hour","h","minutes","minute","m","seconds","second","s","milliseconds","millisecond","ms"];for(t=0;tn.valueOf():n.valueOf()9999?U(n,t?"YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYYYY-MM-DD[T]HH:mm:ss.SSSZ"):H(Date.prototype.toISOString)?t?this.toDate().toISOString():new Date(this.valueOf()+60*this.utcOffset()*1e3).toISOString().replace("Z",U(n,"Z")):U(n,t?"YYYY-MM-DD[T]HH:mm:ss.SSS[Z]":"YYYY-MM-DD[T]HH:mm:ss.SSSZ")}function Jr(){if(!this.isValid())return"moment.invalid(/* "+this._i+" */)";var e,t,n,r,o="moment",c="";return this.isLocal()||(o=0===this.utcOffset()?"moment.utc":"moment.parseZone",c="Z"),e="["+o+'("]',t=0<=this.year()&&this.year()<=9999?"YYYY":"YYYYYY",n="-MM-DD[T]HH:mm:ss.SSS",r=c+'[")]',this.format(e+t+n+r)}function eo(e){e||(e=this.isUtc()?r.defaultFormatUtc:r.defaultFormat);var t=U(this,e);return this.localeData().postformat(t)}function to(e,t){return this.isValid()&&(C(e)&&e.isValid()||Gn(e).isValid())?xr({to:this,from:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()}function no(e){return this.from(Gn(),e)}function ro(e,t){return this.isValid()&&(C(e)&&e.isValid()||Gn(e).isValid())?xr({from:this,to:e}).locale(this.locale()).humanize(!t):this.localeData().invalidDate()}function oo(e){return this.to(Gn(),e)}function co(e){var t;return void 0===e?this._locale._abbr:(null!=(t=yn(e))&&(this._locale=t),this)}r.defaultFormat="YYYY-MM-DDTHH:mm:ssZ",r.defaultFormatUtc="YYYY-MM-DDTHH:mm:ss[Z]";var io=S("moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.",(function(e){return void 0===e?this.localeData():this.locale(e)}));function ao(){return this._locale}var lo=1e3,uo=60*lo,so=60*uo,fo=3506328*so;function po(e,t){return(e%t+t)%t}function ho(e,t,n){return e<100&&e>=0?new Date(e+400,t,n)-fo:new Date(e,t,n).valueOf()}function vo(e,t,n){return e<100&&e>=0?Date.UTC(e+400,t,n)-fo:Date.UTC(e,t,n)}function mo(e){var t,n;if(void 0===(e=re(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?vo:ho,e){case"year":t=n(this.year(),0,1);break;case"quarter":t=n(this.year(),this.month()-this.month()%3,1);break;case"month":t=n(this.year(),this.month(),1);break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday());break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1));break;case"day":case"date":t=n(this.year(),this.month(),this.date());break;case"hour":t=this._d.valueOf(),t-=po(t+(this._isUTC?0:this.utcOffset()*uo),so);break;case"minute":t=this._d.valueOf(),t-=po(t,uo);break;case"second":t=this._d.valueOf(),t-=po(t,lo)}return this._d.setTime(t),r.updateOffset(this,!0),this}function yo(e){var t,n;if(void 0===(e=re(e))||"millisecond"===e||!this.isValid())return this;switch(n=this._isUTC?vo:ho,e){case"year":t=n(this.year()+1,0,1)-1;break;case"quarter":t=n(this.year(),this.month()-this.month()%3+3,1)-1;break;case"month":t=n(this.year(),this.month()+1,1)-1;break;case"week":t=n(this.year(),this.month(),this.date()-this.weekday()+7)-1;break;case"isoWeek":t=n(this.year(),this.month(),this.date()-(this.isoWeekday()-1)+7)-1;break;case"day":case"date":t=n(this.year(),this.month(),this.date()+1)-1;break;case"hour":t=this._d.valueOf(),t+=so-po(t+(this._isUTC?0:this.utcOffset()*uo),so)-1;break;case"minute":t=this._d.valueOf(),t+=uo-po(t,uo)-1;break;case"second":t=this._d.valueOf(),t+=lo-po(t,lo)-1}return this._d.setTime(t),r.updateOffset(this,!0),this}function bo(){return this._d.valueOf()-6e4*(this._offset||0)}function go(){return Math.floor(this.valueOf()/1e3)}function wo(){return new Date(this.valueOf())}function zo(){var e=this;return[e.year(),e.month(),e.date(),e.hour(),e.minute(),e.second(),e.millisecond()]}function Oo(){var e=this;return{years:e.year(),months:e.month(),date:e.date(),hours:e.hours(),minutes:e.minutes(),seconds:e.seconds(),milliseconds:e.milliseconds()}}function Co(){return this.isValid()?this.toISOString():null}function Mo(){return y(this)}function So(){return h({},m(this))}function _o(){return m(this).overflow}function xo(){return{input:this._i,format:this._f,locale:this._locale,isUTC:this._isUTC,strict:this._strict}}function ko(e,t){var n,o,c,i=this._eras||yn("en")._eras;for(n=0,o=i.length;n=0)return l[r]}function Eo(e,t){var n=e.since<=e.until?1:-1;return void 0===t?r(e.since).year():r(e.since).year()+(t-e.offset)*n}function Po(){var e,t,n,r=this.localeData().eras();for(e=0,t=r.length;e(c=Ct(e,r,o))&&(t=c),Xo.call(this,e,t,n,r,o))}function Xo(e,t,n,r,o){var c=zt(e,t,n,r,o),i=gt(c.year,0,c.dayOfYear);return this.year(i.getUTCFullYear()),this.month(i.getUTCMonth()),this.date(i.getUTCDate()),this}function Zo(e){return null==e?Math.ceil((this.month()+1)/3):this.month(3*(e-1)+this.month()%3)}I("N",0,0,"eraAbbr"),I("NN",0,0,"eraAbbr"),I("NNN",0,0,"eraAbbr"),I("NNNN",0,0,"eraName"),I("NNNNN",0,0,"eraNarrow"),I("y",["y",1],"yo","eraYear"),I("y",["yy",2],0,"eraYear"),I("y",["yyy",3],0,"eraYear"),I("y",["yyyy",4],0,"eraYear"),Le("N",Ro),Le("NN",Ro),Le("NNN",Ro),Le("NNNN",Ao),Le("NNNNN",Io),Ae(["N","NN","NNN","NNNN","NNNNN"],(function(e,t,n,r){var o=n._locale.erasParse(e,r,n._strict);o?m(n).era=o:m(n).invalidEra=e})),Le("y",ke),Le("yy",ke),Le("yyy",ke),Le("yyyy",ke),Le("yo",Fo),Ae(["y","yy","yyy","yyyy"],Ue),Ae(["yo"],(function(e,t,n,r){var o;n._locale._eraYearOrdinalRegex&&(o=e.match(n._locale._eraYearOrdinalRegex)),n._locale.eraYearOrdinalParse?t[Ue]=n._locale.eraYearOrdinalParse(e,o):t[Ue]=parseInt(e,10)})),I(0,["gg",2],0,(function(){return this.weekYear()%100})),I(0,["GG",2],0,(function(){return this.isoWeekYear()%100})),Uo("gggg","weekYear"),Uo("ggggg","weekYear"),Uo("GGGG","isoWeekYear"),Uo("GGGGG","isoWeekYear"),ne("weekYear","gg"),ne("isoWeekYear","GG"),ie("weekYear",1),ie("isoWeekYear",1),Le("G",He),Le("g",He),Le("GG",Oe,be),Le("gg",Oe,be),Le("GGGG",_e,we),Le("gggg",_e,we),Le("GGGGG",xe,ze),Le("ggggg",xe,ze),Ie(["gggg","ggggg","GGGG","GGGGG"],(function(e,t,n,r){t[r.substr(0,2)]=se(e)})),Ie(["gg","GG"],(function(e,t,n,o){t[o]=r.parseTwoDigitYear(e)})),I("Q",0,"Qo","quarter"),ne("quarter","Q"),ie("quarter",7),Le("Q",ye),Ae("Q",(function(e,t){t[Ke]=3*(se(e)-1)})),I("D",["DD",2],"Do","date"),ne("date","D"),ie("date",9),Le("D",Oe),Le("DD",Oe,be),Le("Do",(function(e,t){return e?t._dayOfMonthOrdinalParse||t._ordinalParse:t._dayOfMonthOrdinalParseLenient})),Ae(["D","DD"],Be),Ae("Do",(function(e,t){t[Be]=se(e.match(Oe)[0])}));var Jo=fe("Date",!0);function ec(e){var t=Math.round((this.clone().startOf("day")-this.clone().startOf("year"))/864e5)+1;return null==e?t:this.add(e-t,"d")}I("DDD",["DDDD",3],"DDDo","dayOfYear"),ne("dayOfYear","DDD"),ie("dayOfYear",4),Le("DDD",Se),Le("DDDD",ge),Ae(["DDD","DDDD"],(function(e,t,n){n._dayOfYear=se(e)})),I("m",["mm",2],0,"minute"),ne("minute","m"),ie("minute",14),Le("m",Oe),Le("mm",Oe,be),Ae(["m","mm"],qe);var tc=fe("Minutes",!1);I("s",["ss",2],0,"second"),ne("second","s"),ie("second",15),Le("s",Oe),Le("ss",Oe,be),Ae(["s","ss"],Ge);var nc,rc,oc=fe("Seconds",!1);for(I("S",0,0,(function(){return~~(this.millisecond()/100)})),I(0,["SS",2],0,(function(){return~~(this.millisecond()/10)})),I(0,["SSS",3],0,"millisecond"),I(0,["SSSS",4],0,(function(){return 10*this.millisecond()})),I(0,["SSSSS",5],0,(function(){return 100*this.millisecond()})),I(0,["SSSSSS",6],0,(function(){return 1e3*this.millisecond()})),I(0,["SSSSSSS",7],0,(function(){return 1e4*this.millisecond()})),I(0,["SSSSSSSS",8],0,(function(){return 1e5*this.millisecond()})),I(0,["SSSSSSSSS",9],0,(function(){return 1e6*this.millisecond()})),ne("millisecond","ms"),ie("millisecond",16),Le("S",Se,ye),Le("SS",Se,be),Le("SSS",Se,ge),nc="SSSS";nc.length<=9;nc+="S")Le(nc,ke);function cc(e,t){t[$e]=se(1e3*("0."+e))}for(nc="S";nc.length<=9;nc+="S")Ae(nc,cc);function ic(){return this._isUTC?"UTC":""}function ac(){return this._isUTC?"Coordinated Universal Time":""}rc=fe("Milliseconds",!1),I("z",0,0,"zoneAbbr"),I("zz",0,0,"zoneName");var lc=O.prototype;function uc(e){return Gn(1e3*e)}function sc(){return Gn.apply(null,arguments).parseZone()}function fc(e){return e}lc.add=Tr,lc.calendar=Fr,lc.clone=Wr,lc.diff=$r,lc.endOf=yo,lc.format=eo,lc.from=to,lc.fromNow=no,lc.to=ro,lc.toNow=oo,lc.get=de,lc.invalidAt=_o,lc.isAfter=Ur,lc.isBefore=Kr,lc.isBetween=Br,lc.isSame=Yr,lc.isSameOrAfter=qr,lc.isSameOrBefore=Gr,lc.isValid=Mo,lc.lang=io,lc.locale=co,lc.localeData=ao,lc.max=Qn,lc.min=$n,lc.parsingFlags=So,lc.set=ve,lc.startOf=mo,lc.subtract=Lr,lc.toArray=zo,lc.toObject=Oo,lc.toDate=wo,lc.toISOString=Zr,lc.inspect=Jr,"undefined"!==typeof Symbol&&null!=Symbol.for&&(lc[Symbol.for("nodejs.util.inspect.custom")]=function(){return"Moment<"+this.format()+">"}),lc.toJSON=Co,lc.toString=Xr,lc.unix=go,lc.valueOf=bo,lc.creationData=xo,lc.eraName=Po,lc.eraNarrow=Vo,lc.eraAbbr=To,lc.eraYear=Lo,lc.year=mt,lc.isLeapYear=yt,lc.weekYear=Ko,lc.isoWeekYear=Bo,lc.quarter=lc.quarters=Zo,lc.month=st,lc.daysInMonth=ft,lc.week=lc.weeks=kt,lc.isoWeek=lc.isoWeeks=Ht,lc.weeksInYear=Go,lc.weeksInWeekYear=$o,lc.isoWeeksInYear=Yo,lc.isoWeeksInISOWeekYear=qo,lc.date=Jo,lc.day=lc.days=Kt,lc.weekday=Bt,lc.isoWeekday=Yt,lc.dayOfYear=ec,lc.hour=lc.hours=rn,lc.minute=lc.minutes=tc,lc.second=lc.seconds=oc,lc.millisecond=lc.milliseconds=rc,lc.utcOffset=dr,lc.utc=mr,lc.local=yr,lc.parseZone=br,lc.hasAlignedHourOffset=gr,lc.isDST=wr,lc.isLocal=Or,lc.isUtcOffset=Cr,lc.isUtc=Mr,lc.isUTC=Mr,lc.zoneAbbr=ic,lc.zoneName=ac,lc.dates=S("dates accessor is deprecated. Use date instead.",Jo),lc.months=S("months accessor is deprecated. Use month instead",st),lc.years=S("years accessor is deprecated. Use year instead",mt),lc.zone=S("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",vr),lc.isDSTShifted=S("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",zr);var pc=V.prototype;function hc(e,t,n,r){var o=yn(),c=d().set(r,t);return o[n](c,e)}function dc(e,t,n){if(s(e)&&(t=e,e=void 0),e=e||"",null!=t)return hc(e,t,n,"month");var r,o=[];for(r=0;r<12;r++)o[r]=hc(e,r,n,"month");return o}function vc(e,t,n,r){"boolean"===typeof e?(s(t)&&(n=t,t=void 0),t=t||""):(n=t=e,e=!1,s(t)&&(n=t,t=void 0),t=t||"");var o,c=yn(),i=e?c._week.dow:0,a=[];if(null!=n)return hc(t,(n+i)%7,r,"day");for(o=0;o<7;o++)a[o]=hc(t,(o+i)%7,r,"day");return a}function mc(e,t){return dc(e,t,"months")}function yc(e,t){return dc(e,t,"monthsShort")}function bc(e,t,n){return vc(e,t,n,"weekdays")}function gc(e,t,n){return vc(e,t,n,"weekdaysShort")}function wc(e,t,n){return vc(e,t,n,"weekdaysMin")}pc.calendar=L,pc.longDateFormat=Y,pc.invalidDate=G,pc.ordinal=X,pc.preparse=fc,pc.postformat=fc,pc.relativeTime=J,pc.pastFuture=ee,pc.set=E,pc.eras=ko,pc.erasParse=Ho,pc.erasConvertYear=Eo,pc.erasAbbrRegex=No,pc.erasNameRegex=jo,pc.erasNarrowRegex=Do,pc.months=ct,pc.monthsShort=it,pc.monthsParse=lt,pc.monthsRegex=ht,pc.monthsShortRegex=pt,pc.week=Mt,pc.firstDayOfYear=xt,pc.firstDayOfWeek=_t,pc.weekdays=At,pc.weekdaysMin=Ft,pc.weekdaysShort=It,pc.weekdaysParse=Ut,pc.weekdaysRegex=qt,pc.weekdaysShortRegex=Gt,pc.weekdaysMinRegex=$t,pc.isPM=tn,pc.meridiem=on,dn("en",{eras:[{since:"0001-01-01",until:1/0,offset:1,name:"Anno Domini",narrow:"AD",abbr:"AD"},{since:"0000-12-31",until:-1/0,offset:1,name:"Before Christ",narrow:"BC",abbr:"BC"}],dayOfMonthOrdinalParse:/\d{1,2}(th|st|nd|rd)/,ordinal:function(e){var t=e%10;return e+(1===se(e%100/10)?"th":1===t?"st":2===t?"nd":3===t?"rd":"th")}}),r.lang=S("moment.lang is deprecated. Use moment.locale instead.",dn),r.langData=S("moment.langData is deprecated. Use moment.localeData instead.",yn);var zc=Math.abs;function Oc(){var e=this._data;return this._milliseconds=zc(this._milliseconds),this._days=zc(this._days),this._months=zc(this._months),e.milliseconds=zc(e.milliseconds),e.seconds=zc(e.seconds),e.minutes=zc(e.minutes),e.hours=zc(e.hours),e.months=zc(e.months),e.years=zc(e.years),this}function Cc(e,t,n,r){var o=xr(t,n);return e._milliseconds+=r*o._milliseconds,e._days+=r*o._days,e._months+=r*o._months,e._bubble()}function Mc(e,t){return Cc(this,e,t,1)}function Sc(e,t){return Cc(this,e,t,-1)}function _c(e){return e<0?Math.floor(e):Math.ceil(e)}function xc(){var e,t,n,r,o,c=this._milliseconds,i=this._days,a=this._months,l=this._data;return c>=0&&i>=0&&a>=0||c<=0&&i<=0&&a<=0||(c+=864e5*_c(Hc(a)+i),i=0,a=0),l.milliseconds=c%1e3,e=ue(c/1e3),l.seconds=e%60,t=ue(e/60),l.minutes=t%60,n=ue(t/60),l.hours=n%24,i+=ue(n/24),a+=o=ue(kc(i)),i-=_c(Hc(o)),r=ue(a/12),a%=12,l.days=i,l.months=a,l.years=r,this}function kc(e){return 4800*e/146097}function Hc(e){return 146097*e/4800}function Ec(e){if(!this.isValid())return NaN;var t,n,r=this._milliseconds;if("month"===(e=re(e))||"quarter"===e||"year"===e)switch(t=this._days+r/864e5,n=this._months+kc(t),e){case"month":return n;case"quarter":return n/3;case"year":return n/12}else switch(t=this._days+Math.round(Hc(this._months)),e){case"week":return t/7+r/6048e5;case"day":return t+r/864e5;case"hour":return 24*t+r/36e5;case"minute":return 1440*t+r/6e4;case"second":return 86400*t+r/1e3;case"millisecond":return Math.floor(864e5*t)+r;default:throw new Error("Unknown unit "+e)}}function Pc(){return this.isValid()?this._milliseconds+864e5*this._days+this._months%12*2592e6+31536e6*se(this._months/12):NaN}function Vc(e){return function(){return this.as(e)}}var Tc=Vc("ms"),Lc=Vc("s"),jc=Vc("m"),Nc=Vc("h"),Dc=Vc("d"),Rc=Vc("w"),Ac=Vc("M"),Ic=Vc("Q"),Fc=Vc("y");function Wc(){return xr(this)}function Uc(e){return e=re(e),this.isValid()?this[e+"s"]():NaN}function Kc(e){return function(){return this.isValid()?this._data[e]:NaN}}var Bc=Kc("milliseconds"),Yc=Kc("seconds"),qc=Kc("minutes"),Gc=Kc("hours"),$c=Kc("days"),Qc=Kc("months"),Xc=Kc("years");function Zc(){return ue(this.days()/7)}var Jc=Math.round,ei={ss:44,s:45,m:45,h:22,d:26,w:null,M:11};function ti(e,t,n,r,o){return o.relativeTime(t||1,!!n,e,r)}function ni(e,t,n,r){var o=xr(e).abs(),c=Jc(o.as("s")),i=Jc(o.as("m")),a=Jc(o.as("h")),l=Jc(o.as("d")),u=Jc(o.as("M")),s=Jc(o.as("w")),f=Jc(o.as("y")),p=c<=n.ss&&["s",c]||c0,p[4]=r,ti.apply(null,p)}function ri(e){return void 0===e?Jc:"function"===typeof e&&(Jc=e,!0)}function oi(e,t){return void 0!==ei[e]&&(void 0===t?ei[e]:(ei[e]=t,"s"===e&&(ei.ss=t-1),!0))}function ci(e,t){if(!this.isValid())return this.localeData().invalidDate();var n,r,o=!1,c=ei;return"object"===typeof e&&(t=e,e=!1),"boolean"===typeof e&&(o=e),"object"===typeof t&&(c=Object.assign({},ei,t),null!=t.s&&null==t.ss&&(c.ss=t.s-1)),r=ni(this,!o,c,n=this.localeData()),o&&(r=n.pastFuture(+this,r)),n.postformat(r)}var ii=Math.abs;function ai(e){return(e>0)-(e<0)||+e}function li(){if(!this.isValid())return this.localeData().invalidDate();var e,t,n,r,o,c,i,a,l=ii(this._milliseconds)/1e3,u=ii(this._days),s=ii(this._months),f=this.asSeconds();return f?(e=ue(l/60),t=ue(e/60),l%=60,e%=60,n=ue(s/12),s%=12,r=l?l.toFixed(3).replace(/\.?0+$/,""):"",o=f<0?"-":"",c=ai(this._months)!==ai(f)?"-":"",i=ai(this._days)!==ai(f)?"-":"",a=ai(this._milliseconds)!==ai(f)?"-":"",o+"P"+(n?c+n+"Y":"")+(s?c+s+"M":"")+(u?i+u+"D":"")+(t||e||l?"T":"")+(t?a+t+"H":"")+(e?a+e+"M":"")+(l?a+r+"S":"")):"P0D"}var ui=cr.prototype;return ui.isValid=rr,ui.abs=Oc,ui.add=Mc,ui.subtract=Sc,ui.as=Ec,ui.asMilliseconds=Tc,ui.asSeconds=Lc,ui.asMinutes=jc,ui.asHours=Nc,ui.asDays=Dc,ui.asWeeks=Rc,ui.asMonths=Ac,ui.asQuarters=Ic,ui.asYears=Fc,ui.valueOf=Pc,ui._bubble=xc,ui.clone=Wc,ui.get=Uc,ui.milliseconds=Bc,ui.seconds=Yc,ui.minutes=qc,ui.hours=Gc,ui.days=$c,ui.weeks=Zc,ui.months=Qc,ui.years=Xc,ui.humanize=ci,ui.toISOString=li,ui.toString=li,ui.toJSON=li,ui.locale=co,ui.localeData=ao,ui.toIsoString=S("toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)",li),ui.lang=io,I("X",0,0,"unix"),I("x",0,0,"valueOf"),Le("x",He),Le("X",Ve),Ae("X",(function(e,t,n){n._d=new Date(1e3*parseFloat(e))})),Ae("x",(function(e,t,n){n._d=new Date(se(e))})),r.version="2.29.1",o(Gn),r.fn=lc,r.min=Zn,r.max=Jn,r.now=er,r.utc=d,r.unix=uc,r.months=mc,r.isDate=f,r.locale=dn,r.invalid=b,r.duration=xr,r.isMoment=C,r.weekdays=bc,r.parseZone=sc,r.localeData=yn,r.isDuration=ir,r.monthsShort=yc,r.weekdaysMin=wc,r.defineLocale=vn,r.updateLocale=mn,r.locales=bn,r.weekdaysShort=gc,r.normalizeUnits=re,r.relativeTimeRounding=ri,r.relativeTimeThreshold=oi,r.calendarFormat=Ir,r.prototype=lc,r.HTML5_FMT={DATETIME_LOCAL:"YYYY-MM-DDTHH:mm",DATETIME_LOCAL_SECONDS:"YYYY-MM-DDTHH:mm:ss",DATETIME_LOCAL_MS:"YYYY-MM-DDTHH:mm:ss.SSS",DATE:"YYYY-MM-DD",TIME:"HH:mm",TIME_SECONDS:"HH:mm:ss",TIME_MS:"HH:mm:ss.SSS",WEEK:"GGGG-[W]WW",MONTH:"YYYY-MM"},r}()}).call(this,n(72)(e))},function(e,t,n){"use strict";n.p},function(e,t,n){"use strict";t.__esModule=!0,t.default=function(e,t){var n={};for(var r in e)t.indexOf(r)>=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}},function(e,t,n){"use strict";n.d(t,"a",(function(){return r}));var r=function(){for(var e=arguments.length,t=new Array(e),n=0;n children");r=e}})),r}var C=n(9),M=n.n(C),S=n(45),_={isAppearSupported:function(e){return e.transitionName&&e.transitionAppear||e.animation.appear},isEnterSupported:function(e){return e.transitionName&&e.transitionEnter||e.animation.enter},isLeaveSupported:function(e){return e.transitionName&&e.transitionLeave||e.animation.leave},allowAppearCallback:function(e){return e.transitionAppear||e.animation.appear},allowEnterCallback:function(e){return e.transitionEnter||e.animation.enter},allowLeaveCallback:function(e){return e.transitionLeave||e.animation.leave}},x={enter:"transitionEnter",appear:"transitionAppear",leave:"transitionLeave"},k=function(e){function t(){return l()(this,t),p()(this,(t.__proto__||Object.getPrototypeOf(t)).apply(this,arguments))}return d()(t,e),s()(t,[{key:"componentWillUnmount",value:function(){this.stop()}},{key:"componentWillEnter",value:function(e){_.isEnterSupported(this.props)?this.transition("enter",e):e()}},{key:"componentWillAppear",value:function(e){_.isAppearSupported(this.props)?this.transition("appear",e):e()}},{key:"componentWillLeave",value:function(e){_.isLeaveSupported(this.props)?this.transition("leave",e):e()}},{key:"transition",value:function(e,t){var n=this,r=M.a.findDOMNode(this),o=this.props,c=o.transitionName,i="object"===typeof c;this.stop();var a=function(){n.stopper=null,t()};if((S.b||!o.animation[e])&&c&&o[x[e]]){var l=i?c[e]:c+"-"+e,u=l+"-active";i&&c[e+"Active"]&&(u=c[e+"Active"]),this.stopper=Object(S.a)(r,{name:l,active:u},a)}else this.stopper=o.animation[e](r,a)}},{key:"stop",value:function(){var e=this.stopper;e&&(this.stopper=null,e.stop())}},{key:"render",value:function(){return this.props.children}}]),t}(m.a.Component);k.propTypes={children:b.a.any,animation:b.a.any,transitionName:b.a.any};var H=k,E="rc_animate_"+Date.now();function P(e){var t=e.children;return m.a.isValidElement(t)&&!t.key?m.a.cloneElement(t,{key:E}):t}function V(){}var T=function(e){function t(e){l()(this,t);var n=p()(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));return L.call(n),n.currentlyAnimatingKeys={},n.keysToEnter=[],n.keysToLeave=[],n.state={children:w(P(e))},n.childrenRefs={},n}return d()(t,e),s()(t,[{key:"componentDidMount",value:function(){var e=this,t=this.props.showProp,n=this.state.children;t&&(n=n.filter((function(e){return!!e.props[t]}))),n.forEach((function(t){t&&e.performAppear(t.key)}))}},{key:"componentWillReceiveProps",value:function(e){var t=this;this.nextProps=e;var n=w(P(e)),r=this.props;r.exclusive&&Object.keys(this.currentlyAnimatingKeys).forEach((function(e){t.stop(e)}));var o=r.showProp,c=this.currentlyAnimatingKeys,a=r.exclusive?w(P(r)):this.state.children,l=[];o?(a.forEach((function(e){var t=e&&z(n,e.key),r=void 0;(r=t&&t.props[o]||!e.props[o]?t:m.a.cloneElement(t||e,i()({},o,!0)))&&l.push(r)})),n.forEach((function(e){e&&z(a,e.key)||l.push(e)}))):l=function(e,t){var n=[],r={},o=[];return e.forEach((function(e){e&&z(t,e.key)?o.length&&(r[e.key]=o,o=[]):o.push(e)})),t.forEach((function(e){e&&Object.prototype.hasOwnProperty.call(r,e.key)&&(n=n.concat(r[e.key])),n.push(e)})),n=n.concat(o)}(a,n),this.setState({children:l}),n.forEach((function(e){var n=e&&e.key;if(!e||!c[n]){var r=e&&z(a,n);if(o){var i=e.props[o];if(r)!O(a,n,o)&&i&&t.keysToEnter.push(n);else i&&t.keysToEnter.push(n)}else r||t.keysToEnter.push(n)}})),a.forEach((function(e){var r=e&&e.key;if(!e||!c[r]){var i=e&&z(n,r);if(o){var a=e.props[o];if(i)!O(n,r,o)&&a&&t.keysToLeave.push(r);else a&&t.keysToLeave.push(r)}else i||t.keysToLeave.push(r)}}))}},{key:"componentDidUpdate",value:function(){var e=this.keysToEnter;this.keysToEnter=[],e.forEach(this.performEnter);var t=this.keysToLeave;this.keysToLeave=[],t.forEach(this.performLeave)}},{key:"isValidChildByKey",value:function(e,t){var n=this.props.showProp;return n?O(e,t,n):z(e,t)}},{key:"stop",value:function(e){delete this.currentlyAnimatingKeys[e];var t=this.childrenRefs[e];t&&t.stop()}},{key:"render",value:function(){var e=this,t=this.props;this.nextProps=t;var n=this.state.children,r=null;n&&(r=n.map((function(n){if(null===n||void 0===n)return n;if(!n.key)throw new Error("must set key for children");return m.a.createElement(H,{key:n.key,ref:function(t){e.childrenRefs[n.key]=t},animation:t.animation,transitionName:t.transitionName,transitionEnter:t.transitionEnter,transitionAppear:t.transitionAppear,transitionLeave:t.transitionLeave},n)})));var c=t.component;if(c){var i=t;return"string"===typeof c&&(i=o()({className:t.className,style:t.style},t.componentProps)),m.a.createElement(c,i,r)}return r[0]||null}}]),t}(m.a.Component);T.isAnimate=!0,T.propTypes={className:b.a.string,style:b.a.object,component:b.a.any,componentProps:b.a.object,animation:b.a.object,transitionName:b.a.oneOfType([b.a.string,b.a.object]),transitionEnter:b.a.bool,transitionAppear:b.a.bool,exclusive:b.a.bool,transitionLeave:b.a.bool,onEnd:b.a.func,onEnter:b.a.func,onLeave:b.a.func,onAppear:b.a.func,showProp:b.a.string,children:b.a.node},T.defaultProps={animation:{},component:"span",componentProps:{},transitionEnter:!0,transitionLeave:!0,transitionAppear:!1,onEnd:V,onEnter:V,onLeave:V,onAppear:V};var L=function(){var e=this;this.performEnter=function(t){e.childrenRefs[t]&&(e.currentlyAnimatingKeys[t]=!0,e.childrenRefs[t].componentWillEnter(e.handleDoneAdding.bind(e,t,"enter")))},this.performAppear=function(t){e.childrenRefs[t]&&(e.currentlyAnimatingKeys[t]=!0,e.childrenRefs[t].componentWillAppear(e.handleDoneAdding.bind(e,t,"appear")))},this.handleDoneAdding=function(t,n){var r=e.props;if(delete e.currentlyAnimatingKeys[t],!r.exclusive||r===e.nextProps){var o=w(P(r));e.isValidChildByKey(o,t)?"appear"===n?_.allowAppearCallback(r)&&(r.onAppear(t),r.onEnd(t,!0)):_.allowEnterCallback(r)&&(r.onEnter(t),r.onEnd(t,!0)):e.performLeave(t)}},this.performLeave=function(t){e.childrenRefs[t]&&(e.currentlyAnimatingKeys[t]=!0,e.childrenRefs[t].componentWillLeave(e.handleDoneLeaving.bind(e,t)))},this.handleDoneLeaving=function(t){var n=e.props;if(delete e.currentlyAnimatingKeys[t],!n.exclusive||n===e.nextProps){var r=w(P(n));if(e.isValidChildByKey(r,t))e.performEnter(t);else{var o=function(){_.allowLeaveCallback(n)&&(n.onLeave(t),n.onEnd(t,!1))};!function(e,t,n){var r=e.length===t.length;return r&&e.forEach((function(e,o){var c=t[o];e&&c&&(e&&!c||!e&&c||e.key!==c.key||n&&e.props[n]!==c.props[n])&&(r=!1)})),r}(e.state.children,r,n.showProp)?e.setState({children:r},o):o()}}}};t.a=g(T)},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){"use strict";var r=n(5),o=n.n(r),c=n(10),i=n.n(c),a=n(6),l=n.n(a),u=n(11),s=n.n(u),f=n(0),p=n.n(f),h=n(1),d=n.n(h),v=n(9),m=n.n(v),y=n(12);function b(e,t){for(var n=t;n;){if(n===e)return!0;n=n.parentNode}return!1}var g=n(119),w=n.n(g);function z(e,t,n,r){var o=m.a.unstable_batchedUpdates?function(e){m.a.unstable_batchedUpdates(n,e)}:n;return w()(e,t,o,r)}var O=n(87),C=n(88),M=n(8),S=n.n(M);function _(e,t,n){return n?e[0]===t[0]:e[0]===t[0]&&e[1]===t[1]}function x(e,t){this[e]=t}var k,H=n(14),E=n.n(H);function P(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function V(e){for(var t=1;t=0&&n.left>=0&&n.bottom>n.top&&n.right>n.left?n:null}function ge(e){var t,n,r;if(de.isWindow(e)||9===e.nodeType){var o=de.getWindow(e);t={left:de.getWindowScrollLeft(o),top:de.getWindowScrollTop(o)},n=de.viewportWidth(o),r=de.viewportHeight(o)}else t=de.offset(e),n=de.outerWidth(e),r=de.outerHeight(e);return t.width=n,t.height=r,t}function we(e,t){var n=t.charAt(0),r=t.charAt(1),o=e.width,c=e.height,i=e.left,a=e.top;return"c"===n?a+=c/2:"b"===n&&(a+=c),"c"===r?i+=o/2:"r"===r&&(i+=o),{left:i,top:a}}function ze(e,t,n,r,o){var c=we(t,n[1]),i=we(e,n[0]),a=[i.left-c.left,i.top-c.top];return{left:Math.round(e.left-a[0]+r[0]-o[0]),top:Math.round(e.top-a[1]+r[1]-o[1])}}function Oe(e,t,n){return e.leftn.right}function Ce(e,t,n){return e.topn.bottom}function Me(e,t,n){var r=[];return de.each(e,(function(e){r.push(e.replace(t,(function(e){return n[e]})))})),r}function Se(e,t){return e[t]=-e[t],e}function _e(e,t){return(/%$/.test(e)?parseInt(e.substring(0,e.length-1),10)/100*t:parseInt(e,10))||0}function xe(e,t){e[0]=_e(e[0],t.width),e[1]=_e(e[1],t.height)}function ke(e,t,n,r){var o=n.points,c=n.offset||[0,0],i=n.targetOffset||[0,0],a=n.overflow,l=n.source||e;c=[].concat(c),i=[].concat(i);var u={},s=0,f=be(l,!(!(a=a||{})||!a.alwaysByViewport)),p=ge(l);xe(c,p),xe(i,t);var h=ze(p,t,o,c,i),d=de.merge(p,h);if(f&&(a.adjustX||a.adjustY)&&r){if(a.adjustX&&Oe(h,p,f)){var v=Me(o,/[lr]/gi,{l:"r",r:"l"}),m=Se(c,0),y=Se(i,0);(function(e,t,n){return e.left>n.right||e.left+t.widthn.bottom||e.top+t.height=n.left&&o.left+c.width>n.right&&(c.width-=o.left+c.width-n.right),r.adjustX&&o.left+c.width>n.right&&(o.left=Math.max(n.right-c.width,n.left)),r.adjustY&&o.top=n.top&&o.top+c.height>n.bottom&&(c.height-=o.top+c.height-n.bottom),r.adjustY&&o.top+c.height>n.bottom&&(o.top=Math.max(n.bottom-c.height,n.top)),de.mix(o,c)}(h,p,f,u))}return d.width!==p.width&&de.css(l,"width",de.width(l)+d.width-p.width),d.height!==p.height&&de.css(l,"height",de.height(l)+d.height-p.height),de.offset(l,{left:d.left,top:d.top},{useCssRight:n.useCssRight,useCssBottom:n.useCssBottom,useCssTransform:n.useCssTransform,ignoreShake:n.ignoreShake}),{points:o,offset:c,targetOffset:i,overflow:u}}function He(e,t,n){var r=n.target||t;return ke(e,ge(r),n,!function(e,t){var n=be(e,t),r=ge(e);return!n||r.left+r.width<=n.left||r.top+r.height<=n.top||r.left>=n.right||r.top>=n.bottom}(r,n.overflow&&n.overflow.alwaysByViewport))}function Ee(e,t,n){var r,o,c=de.getDocument(e),i=c.defaultView||c.parentWindow,a=de.getWindowScrollLeft(i),l=de.getWindowScrollTop(i),u=de.viewportWidth(i),s=de.viewportHeight(i),f={left:r="pageX"in t?t.pageX:a+t.clientX,top:o="pageY"in t?t.pageY:l+t.clientY,width:0,height:0},p=r>=0&&r<=a+u&&o>=0&&o<=l+s,h=[n.points[0],"cc"];return ke(e,f,V(V({},n),{},{points:h}),p)}He.__getOffsetParent=me,He.__getVisibleRectForElement=be;function Pe(e){return e&&"object"===typeof e&&e.window===e}function Ve(e,t){var n=Math.floor(e),r=Math.floor(t);return Math.abs(n-r)<=1}function Te(e,t){e!==document.activeElement&&b(t,e)&&e.focus()}function Le(e){return"function"===typeof e&&e?e():null}function je(e){return"object"===typeof e&&e?e:null}var Ne=function(e){function t(){var e,n,r,o;i()(this,t);for(var c=arguments.length,a=Array(c),u=0;u1?(!n&&t&&(r.className+=" "+t),p.a.createElement("div",r)):p.a.Children.only(r.children)},t}(f.Component);Fe.propTypes={children:d.a.any,className:d.a.string,visible:d.a.bool,hiddenClassName:d.a.string};var We=Fe,Ue=function(e){function t(){return i()(this,t),l()(this,e.apply(this,arguments))}return s()(t,e),t.prototype.render=function(){var e=this.props,t=e.className;return e.visible||(t+=" "+e.hiddenClassName),p.a.createElement("div",{className:t,onMouseEnter:e.onMouseEnter,onMouseLeave:e.onMouseLeave,onMouseDown:e.onMouseDown,onTouchStart:e.onTouchStart,style:e.style},p.a.createElement(We,{className:e.prefixCls+"-content",visible:e.visible},e.children))},t}(f.Component);Ue.propTypes={hiddenClassName:d.a.string,className:d.a.string,prefixCls:d.a.string,onMouseEnter:d.a.func,onMouseLeave:d.a.func,onMouseDown:d.a.func,onTouchStart:d.a.func,children:d.a.any};var Ke=Ue,Be=function(e){function t(n){i()(this,t);var r=l()(this,e.call(this,n));return Ye.call(r),r.state={stretchChecked:!1,targetWidth:void 0,targetHeight:void 0},r.savePopupRef=x.bind(r,"popupInstance"),r.saveAlignRef=x.bind(r,"alignInstance"),r}return s()(t,e),t.prototype.componentDidMount=function(){this.rootNode=this.getPopupDomNode(),this.setStretchSize()},t.prototype.componentDidUpdate=function(){this.setStretchSize()},t.prototype.getPopupDomNode=function(){return m.a.findDOMNode(this.popupInstance)},t.prototype.getMaskTransitionName=function(){var e=this.props,t=e.maskTransitionName,n=e.maskAnimation;return!t&&n&&(t=e.prefixCls+"-"+n),t},t.prototype.getTransitionName=function(){var e=this.props,t=e.transitionName;return!t&&e.animation&&(t=e.prefixCls+"-"+e.animation),t},t.prototype.getClassName=function(e){return this.props.prefixCls+" "+this.props.className+" "+e},t.prototype.getPopupElement=function(){var e=this,t=this.savePopupRef,n=this.state,r=n.stretchChecked,c=n.targetHeight,i=n.targetWidth,a=this.props,l=a.align,u=a.visible,s=a.prefixCls,f=a.style,h=a.getClassNameFromAlign,d=a.destroyPopupOnHide,v=a.stretch,m=a.children,y=a.onMouseEnter,b=a.onMouseLeave,g=a.onMouseDown,w=a.onTouchStart,z=this.getClassName(this.currentAlignClassName||h(l)),O=s+"-hidden";u||(this.currentAlignClassName=null);var C={};v&&(-1!==v.indexOf("height")?C.height=c:-1!==v.indexOf("minHeight")&&(C.minHeight=c),-1!==v.indexOf("width")?C.width=i:-1!==v.indexOf("minWidth")&&(C.minWidth=i),r||(C.visibility="hidden",setTimeout((function(){e.alignInstance&&e.alignInstance.forceAlign()}),0)));var M={className:z,prefixCls:s,ref:t,onMouseEnter:y,onMouseLeave:b,onMouseDown:g,onTouchStart:w,style:o()({},C,f,this.getZIndexStyle())};return d?p.a.createElement(Re.a,{component:"",exclusive:!0,transitionAppear:!0,transitionName:this.getTransitionName()},u?p.a.createElement(De,{target:this.getAlignTarget(),key:"popup",ref:this.saveAlignRef,monitorWindowResize:!0,align:l,onAlign:this.onAlign},p.a.createElement(Ke,o()({visible:!0},M),m)):null):p.a.createElement(Re.a,{component:"",exclusive:!0,transitionAppear:!0,transitionName:this.getTransitionName(),showProp:"xVisible"},p.a.createElement(De,{target:this.getAlignTarget(),key:"popup",ref:this.saveAlignRef,monitorWindowResize:!0,xVisible:u,childrenProps:{visible:"xVisible"},disabled:!u,align:l,onAlign:this.onAlign},p.a.createElement(Ke,o()({hiddenClassName:O},M),m)))},t.prototype.getZIndexStyle=function(){var e={},t=this.props;return void 0!==t.zIndex&&(e.zIndex=t.zIndex),e},t.prototype.getMaskElement=function(){var e=this.props,t=void 0;if(e.mask){var n=this.getMaskTransitionName();t=p.a.createElement(We,{style:this.getZIndexStyle(),key:"mask",className:e.prefixCls+"-mask",hiddenClassName:e.prefixCls+"-mask-hidden",visible:e.visible}),n&&(t=p.a.createElement(Re.a,{key:"mask",showProp:"visible",transitionAppear:!0,component:"",transitionName:n},t))}return t},t.prototype.render=function(){return p.a.createElement("div",null,this.getMaskElement(),this.getPopupElement())},t}(f.Component);Be.propTypes={visible:d.a.bool,style:d.a.object,getClassNameFromAlign:d.a.func,onAlign:d.a.func,getRootDomNode:d.a.func,align:d.a.any,destroyPopupOnHide:d.a.bool,className:d.a.string,prefixCls:d.a.string,onMouseEnter:d.a.func,onMouseLeave:d.a.func,onMouseDown:d.a.func,onTouchStart:d.a.func,stretch:d.a.string,children:d.a.node,point:d.a.shape({pageX:d.a.number,pageY:d.a.number})};var Ye=function(){var e=this;this.onAlign=function(t,n){var r=e.props,o=r.getClassNameFromAlign(n);e.currentAlignClassName!==o&&(e.currentAlignClassName=o,t.className=e.getClassName(o)),r.onAlign(t,n)},this.setStretchSize=function(){var t=e.props,n=t.stretch,r=t.getRootDomNode,o=t.visible,c=e.state,i=c.stretchChecked,a=c.targetHeight,l=c.targetWidth;if(n&&o){var u=r();if(u){var s=u.offsetHeight,f=u.offsetWidth;a===s&&l===f&&i||e.setState({stretchChecked:!0,targetHeight:s,targetWidth:f})}}else i&&e.setState({stretchChecked:!1})},this.getTargetElement=function(){return e.props.getRootDomNode()},this.getAlignTarget=function(){var t=e.props.point;return t||e.getTargetElement}},qe=Be;function Ge(){}var $e=["onClick","onMouseDown","onTouchStart","onMouseEnter","onMouseLeave","onFocus","onBlur","onContextMenu"],Qe=!!v.createPortal,Xe={rcTrigger:d.a.shape({onPopupMouseDown:d.a.func})},Ze=function(e){function t(n){i()(this,t);var r=l()(this,e.call(this,n));Je.call(r);var o=void 0;return o="popupVisible"in n?!!n.popupVisible:!!n.defaultPopupVisible,r.state={prevPopupVisible:o,popupVisible:o},$e.forEach((function(e){r["fire"+e]=function(t){r.fireEvents(e,t)}})),r}return s()(t,e),t.prototype.getChildContext=function(){return{rcTrigger:{onPopupMouseDown:this.onPopupMouseDown}}},t.prototype.componentDidMount=function(){this.componentDidUpdate({},{popupVisible:this.state.popupVisible})},t.prototype.componentDidUpdate=function(e,t){var n=this.props,r=this.state;if(Qe||this.renderComponent(null,(function(){t.popupVisible!==r.popupVisible&&n.afterPopupVisibleChange(r.popupVisible)})),r.popupVisible){var o=void 0;return this.clickOutsideHandler||!this.isClickToHide()&&!this.isContextMenuToShow()||(o=n.getDocument(),this.clickOutsideHandler=z(o,"mousedown",this.onDocumentClick)),this.touchOutsideHandler||(o=o||n.getDocument(),this.touchOutsideHandler=z(o,"touchstart",this.onDocumentClick)),!this.contextMenuOutsideHandler1&&this.isContextMenuToShow()&&(o=o||n.getDocument(),this.contextMenuOutsideHandler1=z(o,"scroll",this.onContextMenuClose)),void(!this.contextMenuOutsideHandler2&&this.isContextMenuToShow()&&(this.contextMenuOutsideHandler2=z(window,"blur",this.onContextMenuClose)))}this.clearOutsideHandler()},t.prototype.componentWillUnmount=function(){this.clearDelayTimer(),this.clearOutsideHandler(),clearTimeout(this.mouseDownTimeout)},t.getDerivedStateFromProps=function(e,t){var n=e.popupVisible,r={};return void 0!==n&&t.popupVisible!==n&&(r.popupVisible=n,r.prevPopupVisible=t.popupVisible),r},t.prototype.getPopupDomNode=function(){return this._component&&this._component.getPopupDomNode?this._component.getPopupDomNode():null},t.prototype.getPopupAlign=function(){var e=this.props,t=e.popupPlacement,n=e.popupAlign,r=e.builtinPlacements;return t&&r?function(e,t,n){var r=e[t]||{};return o()({},r,n)}(r,t,n):n},t.prototype.setPopupVisible=function(e,t){var n=this.props.alignPoint,r=this.state.popupVisible;this.clearDelayTimer(),r!==e&&("popupVisible"in this.props||this.setState({popupVisible:e,prevPopupVisible:r}),this.props.onPopupVisibleChange(e)),n&&t&&this.setPoint(t)},t.prototype.delaySetPopupVisible=function(e,t,n){var r=this,o=1e3*t;if(this.clearDelayTimer(),o){var c=n?{pageX:n.pageX,pageY:n.pageY}:null;this.delayTimer=setTimeout((function(){r.setPopupVisible(e,c),r.clearDelayTimer()}),o)}else this.setPopupVisible(e,n)},t.prototype.clearDelayTimer=function(){this.delayTimer&&(clearTimeout(this.delayTimer),this.delayTimer=null)},t.prototype.clearOutsideHandler=function(){this.clickOutsideHandler&&(this.clickOutsideHandler.remove(),this.clickOutsideHandler=null),this.contextMenuOutsideHandler1&&(this.contextMenuOutsideHandler1.remove(),this.contextMenuOutsideHandler1=null),this.contextMenuOutsideHandler2&&(this.contextMenuOutsideHandler2.remove(),this.contextMenuOutsideHandler2=null),this.touchOutsideHandler&&(this.touchOutsideHandler.remove(),this.touchOutsideHandler=null)},t.prototype.createTwoChains=function(e){var t=this.props.children.props,n=this.props;return t[e]&&n[e]?this["fire"+e]:t[e]||n[e]},t.prototype.isClickToShow=function(){var e=this.props,t=e.action,n=e.showAction;return-1!==t.indexOf("click")||-1!==n.indexOf("click")},t.prototype.isContextMenuToShow=function(){var e=this.props,t=e.action,n=e.showAction;return-1!==t.indexOf("contextMenu")||-1!==n.indexOf("contextMenu")},t.prototype.isClickToHide=function(){var e=this.props,t=e.action,n=e.hideAction;return-1!==t.indexOf("click")||-1!==n.indexOf("click")},t.prototype.isMouseEnterToShow=function(){var e=this.props,t=e.action,n=e.showAction;return-1!==t.indexOf("hover")||-1!==n.indexOf("mouseEnter")},t.prototype.isMouseLeaveToHide=function(){var e=this.props,t=e.action,n=e.hideAction;return-1!==t.indexOf("hover")||-1!==n.indexOf("mouseLeave")},t.prototype.isFocusToShow=function(){var e=this.props,t=e.action,n=e.showAction;return-1!==t.indexOf("focus")||-1!==n.indexOf("focus")},t.prototype.isBlurToHide=function(){var e=this.props,t=e.action,n=e.hideAction;return-1!==t.indexOf("focus")||-1!==n.indexOf("blur")},t.prototype.forcePopupAlign=function(){this.state.popupVisible&&this._component&&this._component.alignInstance&&this._component.alignInstance.forceAlign()},t.prototype.fireEvents=function(e,t){var n=this.props.children.props[e];n&&n(t);var r=this.props[e];r&&r(t)},t.prototype.close=function(){this.setPopupVisible(!1)},t.prototype.render=function(){var e=this,t=this.state.popupVisible,n=this.props,r=n.children,o=n.forceRender,c=n.alignPoint,i=n.className,a=p.a.Children.only(r),l={key:"trigger"};this.isContextMenuToShow()?l.onContextMenu=this.onContextMenu:l.onContextMenu=this.createTwoChains("onContextMenu"),this.isClickToHide()||this.isClickToShow()?(l.onClick=this.onClick,l.onMouseDown=this.onMouseDown,l.onTouchStart=this.onTouchStart):(l.onClick=this.createTwoChains("onClick"),l.onMouseDown=this.createTwoChains("onMouseDown"),l.onTouchStart=this.createTwoChains("onTouchStart")),this.isMouseEnterToShow()?(l.onMouseEnter=this.onMouseEnter,c&&(l.onMouseMove=this.onMouseMove)):l.onMouseEnter=this.createTwoChains("onMouseEnter"),this.isMouseLeaveToHide()?l.onMouseLeave=this.onMouseLeave:l.onMouseLeave=this.createTwoChains("onMouseLeave"),this.isFocusToShow()||this.isBlurToHide()?(l.onFocus=this.onFocus,l.onBlur=this.onBlur):(l.onFocus=this.createTwoChains("onFocus"),l.onBlur=this.createTwoChains("onBlur"));var u=S()(a&&a.props&&a.props.className,i);u&&(l.className=u);var s=p.a.cloneElement(a,l);if(!Qe)return p.a.createElement(O.a,{parent:this,visible:t,autoMount:!1,forceRender:o,getComponent:this.getComponent,getContainer:this.getContainer},(function(t){var n=t.renderComponent;return e.renderComponent=n,s}));var f=void 0;return(t||this._component||o)&&(f=p.a.createElement(C.a,{key:"portal",getContainer:this.getContainer,didUpdate:this.handlePortalUpdate},this.getComponent())),[s,f]},t}(p.a.Component);Ze.propTypes={children:d.a.any,action:d.a.oneOfType([d.a.string,d.a.arrayOf(d.a.string)]),showAction:d.a.any,hideAction:d.a.any,getPopupClassNameFromAlign:d.a.any,onPopupVisibleChange:d.a.func,afterPopupVisibleChange:d.a.func,popup:d.a.oneOfType([d.a.node,d.a.func]).isRequired,popupStyle:d.a.object,prefixCls:d.a.string,popupClassName:d.a.string,className:d.a.string,popupPlacement:d.a.string,builtinPlacements:d.a.object,popupTransitionName:d.a.oneOfType([d.a.string,d.a.object]),popupAnimation:d.a.any,mouseEnterDelay:d.a.number,mouseLeaveDelay:d.a.number,zIndex:d.a.number,focusDelay:d.a.number,blurDelay:d.a.number,getPopupContainer:d.a.func,getDocument:d.a.func,forceRender:d.a.bool,destroyPopupOnHide:d.a.bool,mask:d.a.bool,maskClosable:d.a.bool,onPopupAlign:d.a.func,popupAlign:d.a.object,popupVisible:d.a.bool,defaultPopupVisible:d.a.bool,maskTransitionName:d.a.oneOfType([d.a.string,d.a.object]),maskAnimation:d.a.string,stretch:d.a.string,alignPoint:d.a.bool},Ze.contextTypes=Xe,Ze.childContextTypes=Xe,Ze.defaultProps={prefixCls:"rc-trigger-popup",getPopupClassNameFromAlign:function(){return""},getDocument:function(){return window.document},onPopupVisibleChange:Ge,afterPopupVisibleChange:Ge,onPopupAlign:Ge,popupClassName:"",mouseEnterDelay:0,mouseLeaveDelay:.1,focusDelay:0,blurDelay:.15,popupStyle:{},destroyPopupOnHide:!1,popupAlign:{},defaultPopupVisible:!1,mask:!1,maskClosable:!0,action:[],showAction:[],hideAction:[]};var Je=function(){var e=this;this.onMouseEnter=function(t){var n=e.props.mouseEnterDelay;e.fireEvents("onMouseEnter",t),e.delaySetPopupVisible(!0,n,n?null:t)},this.onMouseMove=function(t){e.fireEvents("onMouseMove",t),e.setPoint(t)},this.onMouseLeave=function(t){e.fireEvents("onMouseLeave",t),e.delaySetPopupVisible(!1,e.props.mouseLeaveDelay)},this.onPopupMouseEnter=function(){e.clearDelayTimer()},this.onPopupMouseLeave=function(t){t.relatedTarget&&!t.relatedTarget.setTimeout&&e._component&&e._component.getPopupDomNode&&b(e._component.getPopupDomNode(),t.relatedTarget)||e.delaySetPopupVisible(!1,e.props.mouseLeaveDelay)},this.onFocus=function(t){e.fireEvents("onFocus",t),e.clearDelayTimer(),e.isFocusToShow()&&(e.focusTime=Date.now(),e.delaySetPopupVisible(!0,e.props.focusDelay))},this.onMouseDown=function(t){e.fireEvents("onMouseDown",t),e.preClickTime=Date.now()},this.onTouchStart=function(t){e.fireEvents("onTouchStart",t),e.preTouchTime=Date.now()},this.onBlur=function(t){e.fireEvents("onBlur",t),e.clearDelayTimer(),e.isBlurToHide()&&e.delaySetPopupVisible(!1,e.props.blurDelay)},this.onContextMenu=function(t){t.preventDefault(),e.fireEvents("onContextMenu",t),e.setPopupVisible(!0,t)},this.onContextMenuClose=function(){e.isContextMenuToShow()&&e.close()},this.onClick=function(t){if(e.fireEvents("onClick",t),e.focusTime){var n=void 0;if(e.preClickTime&&e.preTouchTime?n=Math.min(e.preClickTime,e.preTouchTime):e.preClickTime?n=e.preClickTime:e.preTouchTime&&(n=e.preTouchTime),Math.abs(n-e.focusTime)<20)return;e.focusTime=0}e.preClickTime=0,e.preTouchTime=0,e.isClickToShow()&&(e.isClickToHide()||e.isBlurToHide())&&t&&t.preventDefault&&t.preventDefault();var r=!e.state.popupVisible;(e.isClickToHide()&&!r||r&&e.isClickToShow())&&e.setPopupVisible(!e.state.popupVisible,t)},this.onPopupMouseDown=function(){var t=e.context.rcTrigger,n=void 0===t?{}:t;e.hasPopupMouseDown=!0,clearTimeout(e.mouseDownTimeout),e.mouseDownTimeout=setTimeout((function(){e.hasPopupMouseDown=!1}),0),n.onPopupMouseDown&&n.onPopupMouseDown.apply(n,arguments)},this.onDocumentClick=function(t){if(!e.props.mask||e.props.maskClosable){var n=t.target;b(Object(v.findDOMNode)(e),n)||e.hasPopupMouseDown||e.close()}},this.getRootDomNode=function(){return Object(v.findDOMNode)(e)},this.getPopupClassNameFromAlign=function(t){var n=[],r=e.props,o=r.popupPlacement,c=r.builtinPlacements,i=r.prefixCls,a=r.alignPoint,l=r.getPopupClassNameFromAlign;return o&&c&&n.push(function(e,t,n,r){var o=n.points;for(var c in e)if(e.hasOwnProperty(c)&&_(e[c].points,o,r))return t+"-placement-"+c;return""}(c,i,t,a)),l&&n.push(l(t)),n.join(" ")},this.getComponent=function(){var t=e.props,n=t.prefixCls,r=t.destroyPopupOnHide,c=t.popupClassName,i=t.action,a=t.onPopupAlign,l=t.popupAnimation,u=t.popupTransitionName,s=t.popupStyle,f=t.mask,h=t.maskAnimation,d=t.maskTransitionName,v=t.zIndex,m=t.popup,y=t.stretch,b=t.alignPoint,g=e.state,w=g.popupVisible,z=g.point,O=e.getPopupAlign(),C={};return e.isMouseEnterToShow()&&(C.onMouseEnter=e.onPopupMouseEnter),e.isMouseLeaveToHide()&&(C.onMouseLeave=e.onPopupMouseLeave),C.onMouseDown=e.onPopupMouseDown,C.onTouchStart=e.onPopupMouseDown,p.a.createElement(qe,o()({prefixCls:n,destroyPopupOnHide:r,visible:w,point:b&&z,className:c,action:i,align:O,onAlign:a,animation:l,getClassNameFromAlign:e.getPopupClassNameFromAlign},C,{stretch:y,getRootDomNode:e.getRootDomNode,style:s,mask:f,zIndex:v,transitionName:u,maskAnimation:h,maskTransitionName:d,ref:e.savePopup}),"function"===typeof m?m():m)},this.getContainer=function(){var t=e.props,n=document.createElement("div");return n.style.position="absolute",n.style.top="0",n.style.left="0",n.style.width="100%",(t.getPopupContainer?t.getPopupContainer(Object(v.findDOMNode)(e)):t.getDocument().body).appendChild(n),n},this.setPoint=function(t){e.props.alignPoint&&t&&e.setState({point:{pageX:t.pageX,pageY:t.pageY}})},this.handlePortalUpdate=function(){e.state.prevPopupVisible!==e.state.popupVisible&&e.props.afterPopupVisibleChange(e.state.popupVisible)},this.savePopup=function(t){e._component=t}};Object(y.polyfill)(Ze);t.a=Ze},function(e,t){var n=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(e,t){var n=e.exports={version:"2.6.12"};"number"==typeof __e&&(__e=n)},function(e,t,n){e.exports=!n(63)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},function(e,t,n){"use strict";var r=n(89),o={placeholder:"Select time"};function c(){return(c=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{};return Object.keys(e).reduce((function(t,n){var r=e[n];switch(n){case"class":t.className=r,delete t.class;break;default:t[n]=r}return t}),{})}var d=function(){function e(){i()(this,e),this.collection={}}return l()(e,[{key:"clear",value:function(){this.collection={}}},{key:"delete",value:function(e){return delete this.collection[e]}},{key:"get",value:function(e){return this.collection[e]}},{key:"has",value:function(e){return Boolean(this.collection[e])}},{key:"set",value:function(e,t){return this.collection[e]=t,this}},{key:"size",get:function(){return Object.keys(this.collection).length}}]),e}();function v(e,t,n){return n?s.createElement(e.tag,o()({key:t},h(e.attrs),n),(e.children||[]).map((function(n,r){return v(n,t+"-"+e.tag+"-"+r)}))):s.createElement(e.tag,o()({key:t},h(e.attrs)),(e.children||[]).map((function(n,r){return v(n,t+"-"+e.tag+"-"+r)})))}function m(e){return Object(u.generate)(e)[0]}function y(e,t){switch(t){case"fill":return e+"-fill";case"outline":return e+"-o";case"twotone":return e+"-twotone";default:throw new TypeError("Unknown theme type: "+t+", name: "+e)}}}).call(this,n(135))},function(e,t,n){"use strict";var r=n(0),o=n.n(r),c=n(20),i=n(121),a=n.n(i),l=n(1),u=n.n(l),s=n(3),f=n.n(s),p=n(15),h=n.n(p),d=n(12),v=n(9),m=n.n(v),y=n(28),b=n(18),g=n(90),w=n(8),z=n.n(w);function O(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter((function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable}))),n.push.apply(n,r)}return n}function C(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function M(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if(!(Symbol.iterator in Object(e))&&"[object Arguments]"!==Object.prototype.toString.call(e))return;var n=[],r=!0,o=!1,c=void 0;try{for(var i,a=e[Symbol.iterator]();!(r=(i=a.next()).done)&&(n.push(i.value),!t||n.length!==t);r=!0);}catch(l){o=!0,c=l}finally{try{r||null==a.return||a.return()}finally{if(o)throw c}}return n}(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}()}var S=/iPhone/i,_=/iPod/i,x=/iPad/i,k=/\bAndroid(?:.+)Mobile\b/i,H=/Android/i,E=/\bAndroid(?:.+)SD4930UR\b/i,P=/\bAndroid(?:.+)(?:KF[A-Z]{2,4})\b/i,V=/Windows Phone/i,T=/\bWindows(?:.+)ARM\b/i,L=/BlackBerry/i,j=/BB10/i,N=/Opera Mini/i,D=/\b(CriOS|Chrome)(?:.+)Mobile/i,R=/Mobile(?:.+)Firefox\b/i;function A(e,t){return e.test(t)}function I(e){var t=e||("undefined"!==typeof navigator?navigator.userAgent:""),n=t.split("[FBAN");"undefined"!==typeof n[1]&&(t=M(n,1)[0]);"undefined"!==typeof(n=t.split("Twitter"))[1]&&(t=M(n,1)[0]);var r={apple:{phone:A(S,t)&&!A(V,t),ipod:A(_,t),tablet:!A(S,t)&&A(x,t)&&!A(V,t),device:(A(S,t)||A(_,t)||A(x,t))&&!A(V,t)},amazon:{phone:A(E,t),tablet:!A(E,t)&&A(P,t),device:A(E,t)||A(P,t)},android:{phone:!A(V,t)&&A(E,t)||!A(V,t)&&A(k,t),tablet:!A(V,t)&&!A(E,t)&&!A(k,t)&&(A(P,t)||A(H,t)),device:!A(V,t)&&(A(E,t)||A(P,t)||A(k,t)||A(H,t))||A(/\bokhttp\b/i,t)},windows:{phone:A(V,t),tablet:A(T,t),device:A(V,t)||A(T,t)},other:{blackberry:A(L,t),blackberry10:A(j,t),opera:A(N,t),firefox:A(R,t),chrome:A(D,t),device:A(L,t)||A(j,t)||A(N,t)||A(R,t)||A(D,t)},any:null,phone:null,tablet:null};return r.any=r.apple.device||r.android.device||r.windows.device||r.other.device,r.phone=r.apple.phone||r.android.phone||r.windows.phone,r.tablet=r.apple.tablet||r.android.tablet||r.windows.tablet,r}var F=function(e){for(var t=1;t0&&setTimeout((function(){e.onMotionEnd({deadline:!0})}),r)}}))},e.nextFrame=function(t){e.cancelNextFrame(),e.raf=pe()(t)},e.cancelNextFrame=function(){e.raf&&(pe.a.cancel(e.raf),e.raf=null)},e.state={status:Ce,statusActive:!1,newStatus:!1,statusStyle:null},e.$cacheEle=null,e.node=null,e.raf=null,e}return se()(t,e),ie()(t,[{key:"componentDidMount",value:function(){this.onDomUpdate()}},{key:"componentDidUpdate",value:function(){this.onDomUpdate()}},{key:"componentWillUnmount",value:function(){this._destroyed=!0,this.removeEventListener(this.$cacheEle),this.cancelNextFrame()}},{key:"render",value:function(){var e,t=this.state,n=t.status,o=t.statusActive,c=t.statusStyle,i=this.props,a=i.children,l=i.motionName,u=i.visible,s=i.removeOnLeave,f=i.leavedClassName,p=i.eventProps;return a?n!==Ce&&r(this.props)?a(ne()({},p,{className:z()((e={},ee()(e,Oe(l,n),n!==Ce),ee()(e,Oe(l,n+"-active"),n!==Ce&&o),ee()(e,l,"string"===typeof l),e)),style:c}),this.setNodeRef):u?a(ne()({},p),this.setNodeRef):s?null:a(ne()({},p,{className:f}),this.setNodeRef):null}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n=t.prevProps,o=t.status;if(!r(e))return{};var c=e.visible,i=e.motionAppear,a=e.motionEnter,l=e.motionLeave,u=e.motionLeaveImmediately,s={prevProps:e};return(o===Me&&!i||o===Se&&!a||o===_e&&!l)&&(s.status=Ce,s.statusActive=!1,s.newStatus=!1),!n&&c&&i&&(s.status=Me,s.statusActive=!1,s.newStatus=!0),n&&!n.visible&&c&&a&&(s.status=Se,s.statusActive=!1,s.newStatus=!0),(n&&n.visible&&!c&&l||!n&&u&&!c&&l)&&(s.status=_e,s.statusActive=!1,s.newStatus=!0),s}}]),t}(o.a.Component);return c.propTypes=ne()({},xe,{internalRef:u.a.oneOfType([u.a.object,u.a.func])}),c.defaultProps={visible:!0,motionEnter:!0,motionAppear:!0,motionLeave:!0,removeOnLeave:!0},Object(d.polyfill)(c),n?o.a.forwardRef((function(e,t){return o.a.createElement(c,ne()({internalRef:t},e))})):c}(ze),He={adjustX:1,adjustY:1},Ee={topLeft:{points:["bl","tl"],overflow:He,offset:[0,-7]},bottomLeft:{points:["tl","bl"],overflow:He,offset:[0,7]},leftTop:{points:["tr","tl"],overflow:He,offset:[-4,0]},rightTop:{points:["tl","tr"],overflow:He,offset:[4,0]}};function Pe(e){return(Pe="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ve(e,t){for(var n=0;n=n.subMenuTitle.offsetWidth||(e.style.minWidth="".concat(n.subMenuTitle.offsetWidth,"px"))}},n.saveSubMenuTitle=function(e){n.subMenuTitle=e};var r=e.store,o=e.eventKey,c=r.getState().defaultActiveFirst;n.isRootMenu=!1;var i=!1;return c&&(i=c[o]),Fe(r,o,i),n}var n,o,c;return function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&je(e,t)}(t,e),n=t,(o=[{key:"componentDidMount",value:function(){this.componentDidUpdate()}},{key:"componentDidUpdate",value:function(){var e=this,t=this.props,n=t.mode,r=t.parentMenu,o=t.manualRef;o&&o(this),"horizontal"===n&&r.isRootMenu&&this.props.isOpen&&(this.minWidthTimeout=setTimeout((function(){return e.adjustWidth()}),0))}},{key:"componentWillUnmount",value:function(){var e=this.props,t=e.onDestroy,n=e.eventKey;t&&t(n),this.minWidthTimeout&&clearTimeout(this.minWidthTimeout),this.mouseenterTimeout&&clearTimeout(this.mouseenterTimeout)}},{key:"renderChildren",value:function(e){var t=this,n=this.props,o={mode:"horizontal"===n.mode?"vertical":n.mode,visible:this.props.isOpen,level:n.level+1,inlineIndent:n.inlineIndent,focusable:!1,onClick:this.onSubMenuClick,onSelect:this.onSelect,onDeselect:this.onDeselect,onDestroy:this.onDestroy,selectedKeys:n.selectedKeys,eventKey:"".concat(n.eventKey,"-menu-"),openKeys:n.openKeys,motion:n.motion,onOpenChange:this.onOpenChange,subMenuOpenDelay:n.subMenuOpenDelay,parentMenu:this,subMenuCloseDelay:n.subMenuCloseDelay,forceSubMenuRender:n.forceSubMenuRender,triggerSubMenuAction:n.triggerSubMenuAction,builtinPlacements:n.builtinPlacements,defaultActiveFirst:n.store.getState().defaultActiveFirst[B(n.eventKey)],multiple:n.multiple,prefixCls:n.rootPrefixCls,id:this.internalMenuId,manualRef:this.saveMenuInstance,itemIcon:n.itemIcon,expandIcon:n.expandIcon},c=this.haveRendered;if(this.haveRendered=!0,this.haveOpened=this.haveOpened||o.visible||o.forceSubMenuRender,!this.haveOpened)return r.createElement("div",null);var i=De({},n.motion,{leavedClassName:"".concat(n.rootPrefixCls,"-hidden"),removeOnLeave:!1,motionAppear:c||!o.visible||"inline"!==o.mode});return r.createElement(ke,Object.assign({visible:o.visible},i),(function(n){var c=n.className,i=n.style,a=z()("".concat(o.prefixCls,"-sub"),c);return r.createElement(zt,Object.assign({},o,{id:t.internalMenuId,className:a,style:i}),e)}))}},{key:"render",value:function(){var e,t=De({},this.props),n=t.isOpen,o=this.getPrefixCls(),c="inline"===t.mode,i=z()(o,"".concat(o,"-").concat(t.mode),(Re(e={},t.className,!!t.className),Re(e,this.getOpenClassName(),n),Re(e,this.getActiveClassName(),t.active||n&&!c),Re(e,this.getDisabledClassName(),t.disabled),Re(e,this.getSelectedClassName(),this.isChildrenSelected()),e));this.internalMenuId||(t.eventKey?this.internalMenuId="".concat(t.eventKey,"$Menu"):(Ae+=1,this.internalMenuId="$__$".concat(Ae,"$Menu")));var a={},l={},u={};t.disabled||(a={onMouseLeave:this.onMouseLeave,onMouseEnter:this.onMouseEnter},l={onClick:this.onTitleClick},u={onMouseEnter:this.onTitleMouseEnter,onMouseLeave:this.onTitleMouseLeave});var s={};c&&(s.paddingLeft=t.inlineIndent*t.level);var f={};this.props.isOpen&&(f={"aria-owns":this.internalMenuId});var p=null;"horizontal"!==t.mode&&(p=this.props.expandIcon,"function"===typeof this.props.expandIcon&&(p=r.createElement(this.props.expandIcon,De({},this.props))));var h=r.createElement("div",Object.assign({ref:this.saveSubMenuTitle,style:s,className:"".concat(o,"-title")},u,l,{"aria-expanded":n},f,{"aria-haspopup":"true",title:"string"===typeof t.title?t.title:void 0}),t.title,p||r.createElement("i",{className:"".concat(o,"-arrow")})),d=this.renderChildren(t.children),v=t.parentMenu.isRootMenu?t.parentMenu.props.getPopupContainer:function(e){return e.parentNode},m=Ie[t.mode],y=t.popupOffset?{offset:t.popupOffset}:{},b="inline"===t.mode?"":t.popupClassName,g=t.disabled,w=t.triggerSubMenuAction,O=t.subMenuOpenDelay,C=t.forceSubMenuRender,M=t.subMenuCloseDelay,S=t.builtinPlacements;return G.forEach((function(e){return delete t[e]})),delete t.onClick,r.createElement("li",Object.assign({},t,a,{className:i,role:"menuitem"}),c&&h,c&&d,!c&&r.createElement(Z.a,{prefixCls:o,popupClassName:"".concat(o,"-popup ").concat(b),getPopupContainer:v,builtinPlacements:Object.assign({},Ee,S),popupPlacement:m,popupVisible:n,popupAlign:y,popup:d,action:g?[]:[w],mouseEnterDelay:O,mouseLeaveDelay:M,onPopupVisibleChange:this.onPopupVisibleChange,forceRender:C},h))}}])&&Ve(n.prototype,o),c&&Ve(n,c),t}(r.Component);We.defaultProps={onMouseEnter:U,onMouseLeave:U,onTitleMouseEnter:U,onTitleMouseLeave:U,onTitleClick:U,manualRef:U,mode:"vertical",title:""};var Ue=Object(y.connect)((function(e,t){var n=e.openKeys,r=e.activeKey,o=e.selectedKeys,c=t.eventKey,i=t.subMenuKey;return{isOpen:n.indexOf(c)>-1,active:r[i]===c,selectedKeys:o}}))(We);Ue.isSubMenu=!0;var Ke=Ue;function Be(e){return(Be="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ye(e){return function(e){if(Array.isArray(e)){for(var t=0,n=new Array(e.length);t=0||(o[n]=e[n]);return o}(e,t);if(Object.getOwnPropertySymbols){var c=Object.getOwnPropertySymbols(e);for(r=0;r=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(o[n]=e[n])}return o}function Xe(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Ze(e,t){for(var n=0;n=0}));c.forEach((function(e){Q(e,"display","inline-block")})),e.menuItemSizes=o.map((function(e){return $(e)})),c.forEach((function(e){Q(e,"display","none")})),e.overflowedIndicatorWidth=$(t.children[t.children.length-1]),e.originalTotalWidth=e.menuItemSizes.reduce((function(e,t){return e+t}),0),e.handleResize(),Q(r,"display","none")}}}},e.handleResize=function(){if("horizontal"===e.props.mode){var t=v.findDOMNode(tt(e));if(t){var n=$(t);e.overflowedItems=[];var r,o=0;e.originalTotalWidth>n+.5&&(r=-1,e.menuItemSizes.forEach((function(t){(o+=t)+e.overflowedIndicatorWidth<=n&&(r+=1)}))),e.setState({lastVisibleIndex:r})}}},e}var n,o,c;return function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&nt(e,t)}(t,e),n=t,(o=[{key:"componentDidMount",value:function(){var e=this;if(this.setChildrenWidthAndResize(),1===this.props.level&&"horizontal"===this.props.mode){var t=v.findDOMNode(this);if(!t)return;this.resizeObserver=new X.a((function(t){t.forEach(e.setChildrenWidthAndResize)})),[].slice.call(t.children).concat(t).forEach((function(t){e.resizeObserver.observe(t)})),"undefined"!==typeof MutationObserver&&(this.mutationObserver=new MutationObserver((function(){e.resizeObserver.disconnect(),[].slice.call(t.children).concat(t).forEach((function(t){e.resizeObserver.observe(t)})),e.setChildrenWidthAndResize()})),this.mutationObserver.observe(t,{attributes:!1,childList:!0,subTree:!1}))}}},{key:"componentWillUnmount",value:function(){this.resizeObserver&&this.resizeObserver.disconnect(),this.mutationObserver&&this.mutationObserver.disconnect()}},{key:"renderChildren",value:function(e){var t=this,n=this.state.lastVisibleIndex;return(e||[]).reduce((function(o,c,i){var a=c;if("horizontal"===t.props.mode){var l=t.getOverflowedSubMenuItem(c.props.eventKey,[]);void 0!==n&&-1!==t.props.className.indexOf("".concat(t.props.prefixCls,"-root"))&&(i>n&&(a=r.cloneElement(c,{style:{display:"none"},eventKey:"".concat(c.props.eventKey,"-hidden"),className:"".concat(ot)})),i===n+1&&(t.overflowedItems=e.slice(n+1).map((function(e){return r.cloneElement(e,{key:e.props.eventKey,mode:"vertical-left"})})),l=t.getOverflowedSubMenuItem(c.props.eventKey,t.overflowedItems)));var u=[].concat(Ye(o),[l,a]);return i===e.length-1&&u.push(t.getOverflowedSubMenuItem(c.props.eventKey,[],!0)),u}return[].concat(Ye(o),[a])}),[])}},{key:"render",value:function(){var e=this.props,t=(e.visible,e.prefixCls,e.overflowedIndicator,e.mode,e.level,e.tag),n=e.children,o=(e.theme,Qe(e,["visible","prefixCls","overflowedIndicator","mode","level","tag","children","theme"])),c=t;return r.createElement(c,Object.assign({},o),this.renderChildren(n))}}])&&Ze(n.prototype,o),c&&Ze(n,c),t}(r.Component);ct.defaultProps={tag:"div",className:""};var it=ct;function at(e){return(at="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function lt(){return(lt=Object.assign||function(e){for(var t=1;t=0||Object.prototype.hasOwnProperty.call(e,r)&&(n[r]=e[r]);return n}(e,["prefixCls","transitionName","animation","align","placement","getPopupContainer","showAction","hideAction","overlayClassName","overlayStyle","trigger"]),d=u;return d||-1===p.indexOf("contextMenu")||(d=["click"]),o.a.createElement(Z.a,hn({},h,{prefixCls:t,ref:this.saveTrigger,popupClassName:s,popupStyle:f,builtinPlacements:pn,action:p,showAction:l,hideAction:d||[],popupPlacement:i,popupAlign:c,popupTransitionName:n,popupAnimation:r,popupVisible:this.state.visible,afterPopupVisibleChange:this.afterVisibleChange,popup:this.getMenuElementOrLambda(),onPopupVisibleChange:this.onVisibleChange,getPopupContainer:a}),this.renderChildren())},t}(r.Component);dn.propTypes={minOverlayWidthMatchTrigger:u.a.bool,onVisibleChange:u.a.func,onOverlayClick:u.a.func,prefixCls:u.a.string,children:u.a.any,transitionName:u.a.string,overlayClassName:u.a.string,openClassName:u.a.string,animation:u.a.any,align:u.a.object,overlayStyle:u.a.object,placement:u.a.string,overlay:u.a.oneOfType([u.a.node,u.a.func]),trigger:u.a.array,alignPoint:u.a.bool,showAction:u.a.array,hideAction:u.a.array,getPopupContainer:u.a.func,visible:u.a.bool,defaultVisible:u.a.bool},dn.defaultProps={prefixCls:"rc-dropdown",trigger:["hover"],showAction:[],overlayClassName:"",overlayStyle:{},defaultVisible:!1,onVisibleChange:function(){},placement:"bottomLeft"};var vn=function(){var e=this;this.onClick=function(t){var n=e.props,r=e.getOverlayElement().props;"visible"in n||e.setState({visible:!1}),n.onOverlayClick&&n.onOverlayClick(t),r.onClick&&r.onClick(t)},this.onVisibleChange=function(t){var n=e.props;"visible"in n||e.setState({visible:t}),n.onVisibleChange(t)},this.getMinOverlayWidthMatchTrigger=function(){var t=e.props,n=t.minOverlayWidthMatchTrigger,r=t.alignPoint;return"minOverlayWidthMatchTrigger"in e.props?n:!r},this.getMenuElement=function(){var t=e.props.prefixCls,n=e.getOverlayElement(),r={prefixCls:t+"-menu",onClick:e.onClick};return"string"===typeof n.type&&delete r.prefixCls,o.a.cloneElement(n,r)},this.afterVisibleChange=function(t){if(t&&e.getMinOverlayWidthMatchTrigger()){var n=e.getPopupDomNode(),r=m.a.findDOMNode(e);r&&n&&r.offsetWidth>n.offsetWidth&&(n.style.minWidth=r.offsetWidth+"px",e.trigger&&e.trigger._component&&e.trigger._component.alignInstance&&e.trigger._component.alignInstance.forceAlign())}},this.saveTrigger=function(t){e.trigger=t}};Object(d.polyfill)(dn);var mn=dn,yn=n(59),bn=n(16),gn=n(13),wn=n(27);function zn(e){return(zn="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function On(){return(On=Object.assign||function(e){for(var t=1;t=0?"slide-down":"slide-up"}},{key:"render",value:function(){return r.createElement(yn.a,null,this.renderDropDown)}}])&&Mn(t.prototype,n),o&&Mn(t,o),i}(r.Component);Hn.defaultProps={mouseEnterDelay:.15,mouseLeaveDelay:.1,placement:"bottomLeft"};var En,Pn=n(32),Vn=0,Tn={};function Ln(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:1,n=Vn++,r=t;function o(){(r-=1)<=0?(e(),delete Tn[n]):Tn[n]=pe()(o)}return Tn[n]=pe()(o),n}function jn(e){return(jn="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Nn(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function Dn(e,t){for(var n=0;n=0)){var r=e.props.insertExtraNode;e.extraNode=document.createElement("div");var o=Fn(e).extraNode;o.className="ant-click-animating-node";var c=e.getAttributeName();t.setAttribute(c,"true"),En=En||document.createElement("style"),n&&"#ffffff"!==n&&"rgb(255, 255, 255)"!==n&&Kn(n)&&!/rgba\(\d*, \d*, \d*, 0\)/.test(n)&&"transparent"!==n&&(e.csp&&e.csp.nonce&&(En.nonce=e.csp.nonce),o.style.borderColor=n,En.innerHTML="\n [ant-click-animating-without-extra-node='true']::after, .ant-click-animating-node {\n --antd-wave-shadow-color: ".concat(n,";\n }"),document.body.contains(En)||document.body.appendChild(En)),r&&t.appendChild(o),Pn.a.addStartEventListener(t,e.onTransitionStart),Pn.a.addEndEventListener(t,e.onTransitionEnd)}},e.onTransitionStart=function(t){if(!e.destroy){var n=Object(v.findDOMNode)(Fn(e));t&&t.target===n&&(e.animationStart||e.resetEffect(n))}},e.onTransitionEnd=function(t){t&&"fadeEffect"===t.animationName&&e.resetEffect(t.target)},e.bindAnimationEvent=function(t){if(t&&t.getAttribute&&!t.getAttribute("disabled")&&!(t.className.indexOf("disabled")>=0)){var n=function(n){if("INPUT"!==n.target.tagName&&!Un(n.target)){e.resetEffect(t);var r=getComputedStyle(t).getPropertyValue("border-top-color")||getComputedStyle(t).getPropertyValue("border-color")||getComputedStyle(t).getPropertyValue("background-color");e.clickWaveTimeoutId=window.setTimeout((function(){return e.onClick(t,r)}),0),Ln.cancel(e.animationStartId),e.animationStart=!0,e.animationStartId=Ln((function(){e.animationStart=!1}),10)}};return t.addEventListener("click",n,!0),{cancel:function(){t.removeEventListener("click",n,!0)}}}},e.renderWave=function(t){var n=t.csp,r=e.props.children;return e.csp=n,r},e}return t=i,(n=[{key:"componentDidMount",value:function(){var e=Object(v.findDOMNode)(this);e&&1===e.nodeType&&(this.instance=this.bindAnimationEvent(e))}},{key:"componentWillUnmount",value:function(){this.instance&&this.instance.cancel(),this.clickWaveTimeoutId&&clearTimeout(this.clickWaveTimeoutId),this.destroy=!0}},{key:"getAttributeName",value:function(){return this.props.insertExtraNode?"ant-click-animating":"ant-click-animating-without-extra-node"}},{key:"resetEffect",value:function(e){if(e&&e!==this.extraNode&&e instanceof Element){var t=this.props.insertExtraNode,n=this.getAttributeName();e.setAttribute(n,"false"),En&&(En.innerHTML=""),t&&this.extraNode&&e.contains(this.extraNode)&&e.removeChild(this.extraNode),Pn.a.removeStartEventListener(e,this.onTransitionStart),Pn.a.removeEndEventListener(e,this.onTransitionEnd)}}},{key:"render",value:function(){return r.createElement(yn.a,null,this.renderWave)}}])&&Dn(t.prototype,n),o&&Dn(t,o),i}(r.Component);function Yn(){return(Yn=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&(y=t.getOptions().map((function(e){return r.createElement(Fr,{prefixCls:d,key:e.value.toString(),disabled:"disabled"in e?e.disabled:i.disabled,value:e.value,checked:-1!==a.value.indexOf(e.value),onChange:e.onChange,className:"".concat(v,"-item")},e.label)})));var b=f()(v,u);return r.createElement("div",Ur({className:b,style:s},m),y)},t.state={value:e.value||e.defaultValue||[],registeredValues:[]},t}return t=a,o=[{key:"getDerivedStateFromProps",value:function(e){return"value"in e?{value:e.value||[]}:null}}],(n=[{key:"getChildContext",value:function(){return{checkboxGroup:{toggleOption:this.toggleOption,value:this.state.value,disabled:this.props.disabled,name:this.props.name,registerValue:this.registerValue,cancelValue:this.cancelValue}}}},{key:"shouldComponentUpdate",value:function(e,t){return!h()(this.props,e)||!h()(this.state,t)}},{key:"getOptions",value:function(){return this.props.options.map((function(e){return"string"===typeof e?{label:e,value:e}:e}))}},{key:"render",value:function(){return r.createElement(yn.a,null,this.renderGroup)}}])&&Yr(t.prototype,n),o&&Yr(t,o),a}(r.Component);Jr.defaultProps={options:[]},Jr.propTypes={defaultValue:l.array,value:l.array,options:l.array.isRequired,onChange:l.func},Jr.childContextTypes={checkboxGroup:l.any},Object(d.polyfill)(Jr);var eo=Jr;Fr.Group=eo;var to=Fr;function no(e){return(no="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ro(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function oo(){return(oo=Object.assign||function(e){for(var t=1;t0&&(d=l.map((function(e){return"string"===typeof e?r.createElement(ho,{key:e,prefixCls:s,disabled:t.props.disabled,value:e,checked:t.state.value===e},e):r.createElement(ho,{key:"radio-group-value-options-".concat(e.value),prefixCls:s,disabled:e.disabled||t.props.disabled,value:e.value,checked:t.state.value===e.value},e.label)}))),r.createElement("div",{className:h,style:o.style,onMouseEnter:o.onMouseEnter,onMouseLeave:o.onMouseLeave,id:o.id},d)},"value"in e)n=e.value;else if("defaultValue"in e)n=e.defaultValue;else{var o=Oo(e.children);n=o&&o.value}return t.state={value:n},t}return t=i,o=[{key:"getDerivedStateFromProps",value:function(e){if("value"in e)return{value:e.value};var t=Oo(e.children);return t?{value:t.value}:null}}],(n=[{key:"getChildContext",value:function(){return{radioGroup:{onChange:this.onRadioChange,value:this.state.value,disabled:this.props.disabled,name:this.props.name}}}},{key:"shouldComponentUpdate",value:function(e,t){return!h()(this.props,e)||!h()(this.state,t)}},{key:"render",value:function(){return r.createElement(yn.a,null,this.renderGroup)}}])&&mo(t.prototype,n),o&&mo(t,o),i}(r.Component);Co.defaultProps={buttonStyle:"outline"},Co.childContextTypes={radioGroup:l.any},Object(d.polyfill)(Co);var Mo=Co;function So(e){return(So="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function _o(){return(_o=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0&&void 0!==arguments[0]?arguments[0]:[],t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"children",n=[],r=function e(r){r.forEach((function(r){if(r[t]){var o=Ao({},r);delete o[t],n.push(o),r[t].length>0&&e(r[t])}else n.push(r)}))};return r(e),n}function Fo(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"children";return e.map((function(e,r){var o={};return e[n]&&(o[n]=Fo(e[n],t,n)),Ao(Ao({},t(e,r)),o)}))}function Wo(e,t){return e.reduce((function(e,n){if(t(n)&&e.push(n),n.children){var r=Wo(n.children,t);e.push.apply(e,Do(r))}return e}),[])}function Uo(e){var t=[];return r.Children.forEach(e,(function(e){if(r.isValidElement(e)){var n=Ao({},e.props);e.key&&(n.key=e.key),e.type&&e.type.__ANT_TABLE_COLUMN_GROUP&&(n.children=Uo(n.children)),t.push(n)}})),t}function Ko(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return(e||[]).forEach((function(e){var n=e.value,r=e.children;t[n.toString()]=n,Ko(r,t)})),t}function Bo(e){return(Bo="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Yo(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function qo(e,t){for(var n=0;n=0?delete r[e.key]:r[e.key]=e.keyPath,t.setState({keyPathOfSelectedItem:r})}},t.renderFilterIcon=function(){var e,n=t.props,o=n.column,c=n.locale,i=n.prefixCls,a=n.selectedKeys,l=a&&a.length>0,u=o.filterIcon;"function"===typeof u&&(u=u(l));var s=f()((Yo(e={},"".concat(i,"-selected"),"filtered"in o?o.filtered:l),Yo(e,"".concat(i,"-open"),t.getDropdownVisible()),e));return u?r.isValidElement(u)?r.cloneElement(u,{title:u.props.title||c.filterTitle,className:f()("".concat(i,"-icon"),s,u.props.className),onClick:Jo}):r.createElement("span",{className:f()("".concat(i,"-icon"),s)},u):r.createElement(gn.a,{title:c.filterTitle,type:"filter",theme:"filled",className:s,onClick:Jo})};var n="filterDropdownVisible"in e.column&&e.column.filterDropdownVisible;return t.state={selectedKeys:e.selectedKeys,valueKeys:Ko(e.column.filters),keyPathOfSelectedItem:{},visible:n,prevProps:e},t}return t=i,o=[{key:"getDerivedStateFromProps",value:function(e,t){var n=e.column,r=t.prevProps,o={prevProps:e};return"selectedKeys"in e&&!h()(r.selectedKeys,e.selectedKeys)&&(o.selectedKeys=e.selectedKeys),h()((r.column||{}).filters,(e.column||{}).filters)||(o.valueKeys=Ko(e.column.filters)),"filterDropdownVisible"in n&&(o.visible=n.filterDropdownVisible),o}}],(n=[{key:"componentDidMount",value:function(){var e=this.props.column;this.setNeverShown(e)}},{key:"componentDidUpdate",value:function(){var e=this.props.column;this.setNeverShown(e)}},{key:"getDropdownVisible",value:function(){return!this.neverShown&&this.state.visible}},{key:"setVisible",value:function(e){var t=this.props.column;"filterDropdownVisible"in t||this.setState({visible:e}),t.onFilterDropdownVisibleChange&&t.onFilterDropdownVisibleChange(e)}},{key:"hasSubMenu",value:function(){var e=this.props.column.filters;return(void 0===e?[]:e).some((function(e){return!!(e.children&&e.children.length>0)}))}},{key:"confirmFilter",value:function(){var e=this.props,t=e.column,n=e.selectedKeys,r=e.confirmFilter,o=this.state,c=o.selectedKeys,i=o.valueKeys,a=t.filterDropdown;h()(c,n)||r(t,a?c:c.map((function(e){return i[e]})).filter((function(e){return void 0!==e})))}},{key:"renderMenus",value:function(e){var t=this,n=this.props,o=n.dropdownPrefixCls,c=n.prefixCls;return e.map((function(e){if(e.children&&e.children.length>0){var n=t.state.keyPathOfSelectedItem,i=Object.keys(n).some((function(t){return n[t].indexOf(e.value)>=0})),a=f()("".concat(c,"-dropdown-submenu"),Yo({},"".concat(o,"-submenu-contain-selected"),i));return r.createElement(Ke,{title:e.text,popupClassName:a,key:e.value.toString()},t.renderMenus(e.children))}return t.renderMenuItem(e)}))}},{key:"renderMenuItem",value:function(e){var t=this.props.column,n=this.state.selectedKeys,o=!("filterMultiple"in t)||t.filterMultiple,c=(n||[]).map((function(e){return e.toString()})),i=o?r.createElement(to,{checked:c.indexOf(e.value.toString())>=0}):r.createElement(jo,{checked:c.indexOf(e.value.toString())>=0});return r.createElement(Gt,{key:e.value},i,r.createElement("span",null,e.text))}},{key:"render",value:function(){var e=this,t=this.state.selectedKeys,n=this.props,o=n.column,c=n.locale,i=n.prefixCls,a=n.dropdownPrefixCls,l=n.getPopupContainer,u=!("filterMultiple"in o)||o.filterMultiple,s=f()(Yo({},"".concat(a,"-menu-without-submenu"),!this.hasSubMenu())),p=o.filterDropdown;p instanceof Function&&(p=p({prefixCls:"".concat(a,"-custom"),setSelectedKeys:function(t){return e.setSelectedKeys({selectedKeys:t})},selectedKeys:t,confirm:this.handleConfirm,clearFilters:this.handleClearFilters,filters:o.filters,visible:this.getDropdownVisible()}));var h=p?r.createElement(No,{className:"".concat(i,"-dropdown")},p):r.createElement(No,{className:"".concat(i,"-dropdown")},r.createElement(an,{multiple:u,onClick:this.handleMenuItemClick,prefixCls:"".concat(a,"-menu"),className:s,onSelect:this.setSelectedKeys,onDeselect:this.setSelectedKeys,selectedKeys:t&&t.map((function(e){return e.toString()})),getPopupContainer:l},this.renderMenus(o.filters)),r.createElement("div",{className:"".concat(i,"-dropdown-btns")},r.createElement("a",{className:"".concat(i,"-dropdown-link confirm"),onClick:this.handleConfirm},c.filterConfirm),r.createElement("a",{className:"".concat(i,"-dropdown-link clear"),onClick:this.handleClearFilters},c.filterReset)));return r.createElement(Mr,{trigger:["click"],placement:"bottomRight",overlay:h,visible:this.getDropdownVisible(),onVisibleChange:this.onVisibleChange,getPopupContainer:l,forceRender:!0},this.renderFilterIcon())}}])&&qo(t.prototype,n),o&&qo(t,o),i}(r.Component);ec.defaultProps={column:{}},Object(d.polyfill)(ec);var tc=ec;function nc(){return(nc=Object.assign||function(e){for(var t=1;t=0:t.getState().selectedRowKeys.indexOf(r)>=0||n.indexOf(r)>=0}},{key:"subscribe",value:function(){var e=this,t=this.props.store;this.unsubscribe=t.subscribe((function(){var t=e.getCheckState(e.props);e.setState({checked:t})}))}},{key:"render",value:function(){var e=this.props,t=e.type,n=e.rowIndex,o=sc(e,["type","rowIndex"]),c=this.state.checked;return"radio"===t?r.createElement(jo,oc({checked:c,value:n},o)):r.createElement(to,oc({checked:c},o))}}])&&cc(t.prototype,n),o&&cc(t,o),i}(r.Component),pc=n(29),hc=n.n(pc),dc=hc()({inlineCollapsed:!1});function vc(e){return(vc="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function mc(){return(mc=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n0,t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e));return r.createElement(Ac.Provider,{value:{siderHook:this.getSiderHook()}},r.createElement(u,Hc({className:p},s),a))}}]),n}(r.Component),Uc=Ic({suffixCls:"layout",tagName:"section",displayName:"Layout"})(Wc),Kc=Ic({suffixCls:"layout-header",tagName:"header",displayName:"Header"})(Fc),Bc=Ic({suffixCls:"layout-footer",tagName:"footer",displayName:"Footer"})(Fc),Yc=Ic({suffixCls:"layout-content",tagName:"main",displayName:"Content"})(Fc);Uc.Header=Kc,Uc.Footer=Bc,Uc.Content=Yc;var qc=function(e){return!isNaN(parseFloat(e))&&isFinite(e)};function Gc(e){return(Gc="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function $c(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function Qc(){return(Qc=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:"";return e+=1,"".concat(t).concat(e)}}(),ui=function(e){ei(n,e);var t=ni(n);function n(e){var o,i,a;return Xc(this,n),(o=t.call(this,e)).responsiveHandler=function(e){o.setState({below:e.matches});var t=o.props.onBreakpoint;t&&t(e.matches),o.state.collapsed!==e.matches&&o.setCollapsed(e.matches,"responsive")},o.setCollapsed=function(e,t){"collapsed"in o.props||o.setState({collapsed:e});var n=o.props.onCollapse;n&&n(e,t)},o.toggle=function(){var e=!o.state.collapsed;o.setCollapsed(e,"clickTrigger")},o.belowShowChange=function(){o.setState((function(e){return{belowShow:!e.belowShow}}))},o.renderSider=function(e){var t,n=e.getPrefixCls,i=o.props,a=i.prefixCls,l=i.className,u=i.theme,s=i.collapsible,p=i.reverseArrow,h=i.trigger,d=i.style,v=i.width,m=i.collapsedWidth,y=i.zeroWidthTriggerStyle,b=ci(i,["prefixCls","className","theme","collapsible","reverseArrow","trigger","style","width","collapsedWidth","zeroWidthTriggerStyle"]),g=n("layout-sider",a),w=Object(c.a)(b,["collapsed","defaultCollapsed","onCollapse","breakpoint","onBreakpoint","siderHook","zeroWidthTriggerStyle"]),z=o.state.collapsed?m:v,O=qc(z)?"".concat(z,"px"):String(z),C=0===parseFloat(String(m||0))?r.createElement("span",{onClick:o.toggle,className:"".concat(g,"-zero-width-trigger ").concat(g,"-zero-width-trigger-").concat(p?"right":"left"),style:y},r.createElement(gn.a,{type:"bars"})):null,M={expanded:p?r.createElement(gn.a,{type:"right"}):r.createElement(gn.a,{type:"left"}),collapsed:p?r.createElement(gn.a,{type:"left"}):r.createElement(gn.a,{type:"right"})}[o.state.collapsed?"collapsed":"expanded"],S=null!==h?C||r.createElement("div",{className:"".concat(g,"-trigger"),onClick:o.toggle,style:{width:O}},h||M):null,_=Qc(Qc({},d),{flex:"0 0 ".concat(O),maxWidth:O,minWidth:O,width:O}),x=f()(l,g,"".concat(g,"-").concat(u),($c(t={},"".concat(g,"-collapsed"),!!o.state.collapsed),$c(t,"".concat(g,"-has-trigger"),s&&null!==h&&!C),$c(t,"".concat(g,"-below"),!!o.state.below),$c(t,"".concat(g,"-zero-width"),0===parseFloat(O)),t));return r.createElement("aside",Qc({className:x},w,{style:_}),r.createElement("div",{className:"".concat(g,"-children")},o.props.children),s||o.state.below&&C?S:null)},o.uniqueId=li("ant-sider-"),"undefined"!==typeof window&&(i=window.matchMedia),i&&e.breakpoint&&e.breakpoint in ii&&(o.mql=i("(max-width: ".concat(ii[e.breakpoint],")"))),a="collapsed"in e?e.collapsed:e.defaultCollapsed,o.state={collapsed:a,below:!1},o}return Jc(n,[{key:"componentDidMount",value:function(){this.mql&&(this.mql.addListener(this.responsiveHandler),this.responsiveHandler(this.mql)),this.props.siderHook&&this.props.siderHook.addSider(this.uniqueId)}},{key:"componentWillUnmount",value:function(){this.mql&&this.mql.removeListener(this.responsiveHandler),this.props.siderHook&&this.props.siderHook.removeSider(this.uniqueId)}},{key:"render",value:function(){var e=this.state.collapsed,t=this.props.collapsedWidth;return r.createElement(ai.Provider,{value:{siderCollapsed:e,collapsedWidth:t}},r.createElement(yn.a,null,this.renderSider))}}],[{key:"getDerivedStateFromProps",value:function(e){return"collapsed"in e?{collapsed:e.collapsed}:null}}]),n}(r.Component);ui.defaultProps={collapsible:!1,defaultCollapsed:!1,reverseArrow:!1,width:200,collapsedWidth:80,style:{},theme:"dark"},Object(d.polyfill)(ui);r.Component;function si(e){return(si="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function fi(){return(fi=Object.assign||function(e){for(var t=1;t=0;(t||c)&&o.restoreModeVerticalFromInline()},o.handleClick=function(e){o.handleOpenChange([]);var t=o.props.onClick;t&&t(e)},o.handleOpenChange=function(e){o.setOpenKeys(e);var t=o.props.onOpenChange;t&&t(e)},o.renderMenu=function(e){var t,n,i,a=e.getPopupContainer,l=e.getPrefixCls,u=o.props,s=u.prefixCls,p=u.className,h=u.theme,d=u.collapsedWidth,v=Object(c.a)(o.props,["collapsedWidth","siderCollapsed"]),m=o.getRealMenuMode(),y=o.getOpenMotionProps(m),b=l("menu",s),g=f()(p,"".concat(b,"-").concat(h),(t={},n="".concat(b,"-inline-collapsed"),i=o.getInlineCollapsed(),n in t?Object.defineProperty(t,n,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[n]=i,t)),w=Mi({openKeys:o.state.openKeys,onOpenChange:o.handleOpenChange,className:g,mode:m},y);return"inline"!==m&&(w.onClick=o.handleClick),o.getInlineCollapsed()&&(0===d||"0"===d||"0px"===d)&&(w.openKeys=[]),r.createElement(an,Mi({getPopupContainer:a},v,w,{prefixCls:b,onTransitionEnd:o.handleTransitionEnd,onMouseEnter:o.handleMouseEnter}))},Object(bn.a)(!("onOpen"in e||"onClose"in e),"Menu","`onOpen` and `onClose` are removed, please use `onOpenChange` instead, see: https://u.ant.design/menu-on-open-change."),Object(bn.a)(!("inlineCollapsed"in e&&"inline"!==e.mode),"Menu","`inlineCollapsed` should only be used when `mode` is inline."),Object(bn.a)(!(void 0!==e.siderCollapsed&&"inlineCollapsed"in e),"Menu","`inlineCollapsed` not control Menu under Sider. Should set `collapsed` on Sider instead."),"openKeys"in e?i=e.openKeys:"defaultOpenKeys"in e&&(i=e.defaultOpenKeys),o.state={openKeys:i||[],switchingModeFromInline:!1,inlineOpenKeys:[],prevProps:e},o}return xi(n,[{key:"componentWillUnmount",value:function(){Ln.cancel(this.mountRafId)}},{key:"setOpenKeys",value:function(e){"openKeys"in this.props||this.setState({openKeys:e})}},{key:"getRealMenuMode",value:function(){var e=this.getInlineCollapsed();if(this.state.switchingModeFromInline&&e)return"inline";var t=this.props.mode;return e?"vertical":t}},{key:"getInlineCollapsed",value:function(){var e=this.props.inlineCollapsed;return void 0!==this.props.siderCollapsed?this.props.siderCollapsed:e}},{key:"getOpenMotionProps",value:function(e){var t=this.props,n=t.openTransitionName,r=t.openAnimation,o=t.motion;return o?{motion:o}:r?(Object(bn.a)("string"===typeof r,"Menu","`openAnimation` do not support object. Please use `motion` instead."),{openAnimation:r}):n?{openTransitionName:n}:"horizontal"===e?{motion:{motionName:"slide-up"}}:"inline"===e?{motion:Oi}:{motion:{motionName:this.state.switchingModeFromInline?"":"zoom-big"}}}},{key:"restoreModeVerticalFromInline",value:function(){this.state.switchingModeFromInline&&this.setState({switchingModeFromInline:!1})}},{key:"render",value:function(){return r.createElement(dc.Provider,{value:{inlineCollapsed:this.getInlineCollapsed()||!1,antdMenuTheme:this.props.theme}},r.createElement(yn.a,null,this.renderMenu))}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n=t.prevProps,r={prevProps:e};return"inline"===n.mode&&"inline"!==e.mode&&(r.switchingModeFromInline=!0),"openKeys"in e?r.openKeys=e.openKeys:((e.inlineCollapsed&&!n.inlineCollapsed||e.siderCollapsed&&!n.siderCollapsed)&&(r.switchingModeFromInline=!0,r.inlineOpenKeys=t.openKeys,r.openKeys=[]),(!e.inlineCollapsed&&n.inlineCollapsed||!e.siderCollapsed&&n.siderCollapsed)&&(r.openKeys=t.inlineOpenKeys,r.inlineOpenKeys=[])),r}}]),n}(r.Component);Ti.defaultProps={className:"",theme:"light",focusable:!1},Object(d.polyfill)(Ti);var Li=function(e){ki(n,e);var t=Ei(n);function n(){return Si(this,n),t.apply(this,arguments)}return xi(n,[{key:"render",value:function(){var e=this;return r.createElement(ai.Consumer,null,(function(t){return r.createElement(Ti,Mi({},e.props,t))}))}}]),n}(r.Component);function ji(e){return(ji="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Ni(e,t){for(var n=0;n=0}))}function Ui(e){var t=e.store,n=e.data;if(!n.length)return!1;var r=Wi(Fi(Fi({},e),{data:n,type:"some",byDefaultChecked:!1}))&&!Wi(Fi(Fi({},e),{data:n,type:"every",byDefaultChecked:!1})),o=Wi(Fi(Fi({},e),{data:n,type:"some",byDefaultChecked:!0}))&&!Wi(Fi(Fi({},e),{data:n,type:"every",byDefaultChecked:!0}));return t.getState().selectionDirty?r:r||o}function Ki(e){var t=e.store,n=e.data;return!!n.length&&(t.getState().selectionDirty?Wi(Fi(Fi({},e),{data:n,type:"every",byDefaultChecked:!1})):Wi(Fi(Fi({},e),{data:n,type:"every",byDefaultChecked:!1}))||Wi(Fi(Fi({},e),{data:n,type:"every",byDefaultChecked:!0})))}Li.Divider=cn,Li.Item=gi,Li.SubMenu=Mc,Li.ItemGroup=rn;var Bi=function(e){!function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&Di(e,t)}(i,e);var t,n,o,c=Ri(i);function i(e){var t;return function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,i),(t=c.call(this,e)).state={checked:!1,indeterminate:!1},t.handleSelectAllChange=function(e){var n=e.target.checked;t.props.onSelect(n?"all":"removeAll",0,null)},t.defaultSelections=e.hideDefaultSelections?[]:[{key:"all",text:e.locale.selectAll},{key:"invert",text:e.locale.selectInvert}],t}return t=i,o=[{key:"getDerivedStateFromProps",value:function(e,t){var n=Ki(e),r=Ui(e),o={};return r!==t.indeterminate&&(o.indeterminate=r),n!==t.checked&&(o.checked=n),o}}],(n=[{key:"componentDidMount",value:function(){this.subscribe()}},{key:"componentWillUnmount",value:function(){this.unsubscribe&&this.unsubscribe()}},{key:"setCheckState",value:function(e){var t=Ki(e),n=Ui(e);this.setState((function(e){var r={};return n!==e.indeterminate&&(r.indeterminate=n),t!==e.checked&&(r.checked=t),r}))}},{key:"subscribe",value:function(){var e=this,t=this.props.store;this.unsubscribe=t.subscribe((function(){e.setCheckState(e.props)}))}},{key:"renderMenus",value:function(e){var t=this;return e.map((function(e,n){return r.createElement(Li.Item,{key:e.key||n},r.createElement("div",{onClick:function(){t.props.onSelect(e.key,n,e.onSelect)}},e.text))}))}},{key:"render",value:function(){var e,t,n,o=this.props,c=o.disabled,i=o.prefixCls,a=o.selections,l=o.getPopupContainer,u=this.state,s=u.checked,p=u.indeterminate,h="".concat(i,"-selection"),d=null;if(a){var v=Array.isArray(a)?this.defaultSelections.concat(a):this.defaultSelections,m=r.createElement(Li,{className:"".concat(h,"-menu"),selectedKeys:[]},this.renderMenus(v));d=v.length>0?r.createElement(Mr,{overlay:m,getPopupContainer:l},r.createElement("div",{className:"".concat(h,"-down")},r.createElement(gn.a,{type:"down"}))):null}return r.createElement("div",{className:h},r.createElement(to,{className:f()((e={},t="".concat(h,"-select-all-custom"),n=d,t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e)),checked:s,indeterminate:p,disabled:c,onChange:this.handleSelectAllChange}),d)}}])&&Ni(t.prototype,n),o&&Ni(t,o),i}(r.Component);Object(d.polyfill)(Bi);var Yi=Bi;function qi(e){return(qi="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function Gi(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function $i(e,t){return($i=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function Qi(e){var t=function(){if("undefined"===typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"===typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=Zi(e);if(t){var o=Zi(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return Xi(this,n)}}function Xi(e,t){return!t||"object"!==qi(t)&&"function"!==typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}function Zi(e){return(Zi=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}var Ji=function(e){!function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&$i(e,t)}(n,e);var t=Qi(n);function n(){return Gi(this,n),t.apply(this,arguments)}return n}(r.Component);function ea(e){return(ea="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ta(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function na(e,t){return(na=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e})(e,t)}function ra(e){var t=function(){if("undefined"===typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"===typeof Proxy)return!0;try{return Date.prototype.toString.call(Reflect.construct(Date,[],(function(){}))),!0}catch(e){return!1}}();return function(){var n,r=ca(e);if(t){var o=ca(this).constructor;n=Reflect.construct(r,arguments,o)}else n=r.apply(this,arguments);return oa(this,n)}}function oa(e,t){return!t||"object"!==ea(t)&&"function"!==typeof t?function(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}(e):t}function ca(e){return(ca=Object.setPrototypeOf?Object.getPrototypeOf:function(e){return e.__proto__||Object.getPrototypeOf(e)})(e)}var ia=function(e){!function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&na(e,t)}(n,e);var t=ra(n);function n(){return ta(this,n),t.apply(this,arguments)}return n}(r.Component);function aa(e){return(aa="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function la(){return(la=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:"tr",t=function(t){ha(o,t);var n=va(o);function o(e){var t;sa(this,o),(t=n.call(this,e)).store=e.store;var r=t.store.getState().selectedRowKeys;return t.state={selected:r.indexOf(e.rowKey)>=0},t}return pa(o,[{key:"componentDidMount",value:function(){this.subscribe()}},{key:"componentWillUnmount",value:function(){this.unsubscribe&&this.unsubscribe()}},{key:"subscribe",value:function(){var e=this,t=this.props,n=t.store,r=t.rowKey;this.unsubscribe=n.subscribe((function(){var t=e.store.getState().selectedRowKeys.indexOf(r)>=0;t!==e.state.selected&&e.setState({selected:t})}))}},{key:"render",value:function(){var t=Object(c.a)(this.props,["prefixCls","rowKey","store"]),n=f()(this.props.className,ua({},"".concat(this.props.prefixCls,"-row-selected"),this.state.selected));return r.createElement(e,la(la({},t),{className:n}),this.props.children)}}]),o}(r.Component);return t}function ga(e,t){if("undefined"===typeof window)return 0;var n=t?"scrollTop":"scrollLeft",r=e===window,o=r?e[t?"pageYOffset":"pageXOffset"]:e[n];return r&&"number"!==typeof o&&(o=document.documentElement[n]),o}function wa(e,t,n,r){var o=n-t;return(e/=r/2)<1?o/2*e*e*e+t:o/2*((e-=2)*e*e+2)+t}ia.__ANT_TABLE_COLUMN_GROUP=!0;var za=function(e){var t,n=e.rootPrefixCls+"-item",r=z()(n,n+"-"+e.page,(t={},ee()(t,n+"-active",e.active),ee()(t,e.className,!!e.className),ee()(t,n+"-disabled",!e.page),t));return o.a.createElement("li",{title:e.showTitle?e.page:null,className:r,onClick:function(){e.onClick(e.page)},onKeyPress:function(t){e.onKeyPress(t,e.onClick,e.page)},tabIndex:"0"},e.itemRender(e.page,"page",o.a.createElement("a",null,e.page)))};za.propTypes={page:u.a.number,active:u.a.bool,last:u.a.bool,locale:u.a.object,className:u.a.string,showTitle:u.a.bool,rootPrefixCls:u.a.string,onClick:u.a.func,onKeyPress:u.a.func,itemRender:u.a.func};var Oa=za,Ca=13,Ma=38,Sa=40,_a=function(e){function t(){var e,n,r,o;oe()(this,t);for(var c=arguments.length,i=Array(c),a=0;a=0||e.relatedTarget.className.indexOf(c+"-next")>=0)||o(r.getValidValue())},r.go=function(e){""!==r.state.goInputText&&(e.keyCode!==Ca&&"click"!==e.type||(r.setState({goInputText:""}),r.props.quickGo(r.getValidValue())))},o=n,le()(r,o)}return se()(t,e),ie()(t,[{key:"getValidValue",value:function(){var e=this.state,t=e.goInputText,n=e.current;return!t||isNaN(t)?n:Number(t)}},{key:"render",value:function(){var e=this,t=this.props,n=t.pageSize,r=t.pageSizeOptions,c=t.locale,i=t.rootPrefixCls,a=t.changeSize,l=t.quickGo,u=t.goButton,s=t.selectComponentClass,f=t.buildOptionText,p=t.selectPrefixCls,h=t.disabled,d=this.state.goInputText,v=i+"-options",m=s,y=null,b=null,g=null;if(!a&&!l)return null;if(a&&m){var w=r.map((function(t,n){return o.a.createElement(m.Option,{key:n,value:t},(f||e.buildOptionText)(t))}));y=o.a.createElement(m,{disabled:h,prefixCls:p,showSearch:!1,className:v+"-size-changer",optionLabelProp:"children",dropdownMatchSelectWidth:!1,value:(n||r[0]).toString(),onChange:this.changeSize,getPopupContainer:function(e){return e.parentNode}},w)}return l&&(u&&(g="boolean"===typeof u?o.a.createElement("button",{type:"button",onClick:this.go,onKeyUp:this.go,disabled:h},c.jump_to_confirm):o.a.createElement("span",{onClick:this.go,onKeyUp:this.go},u)),b=o.a.createElement("div",{className:v+"-quick-jumper"},c.jump_to,o.a.createElement("input",{disabled:h,type:"text",value:d,onChange:this.handleChange,onKeyUp:this.go,onBlur:this.handleBlur}),c.page,g)),o.a.createElement("li",{className:""+v},y,b)}}]),t}(o.a.Component);_a.propTypes={disabled:u.a.bool,changeSize:u.a.func,quickGo:u.a.func,selectComponentClass:u.a.func,current:u.a.number,pageSizeOptions:u.a.arrayOf(u.a.string),pageSize:u.a.number,buildOptionText:u.a.func,locale:u.a.object,rootPrefixCls:u.a.string,selectPrefixCls:u.a.string,goButton:u.a.oneOfType([u.a.bool,u.a.node])},_a.defaultProps={pageSizeOptions:["10","20","30","40"]};var xa=_a,ka=n(94);function Ha(){}function Ea(e,t,n){var r=e;return"undefined"===typeof r&&(r=t.pageSize),Math.floor((n.total-1)/r)+1}var Pa=function(e){function t(e){oe()(this,t);var n=le()(this,(t.__proto__||Object.getPrototypeOf(t)).call(this,e));Va.call(n);var r=e.onChange!==Ha;"current"in e&&!r&&console.warn("Warning: You provided a `current` prop to a Pagination component without an `onChange` handler. This will render a read-only component.");var o=e.defaultCurrent;"current"in e&&(o=e.current);var c=e.defaultPageSize;return"pageSize"in e&&(c=e.pageSize),o=Math.min(o,Ea(c,void 0,e)),n.state={current:o,currentInputValue:o,pageSize:c},n}return se()(t,e),ie()(t,[{key:"componentDidUpdate",value:function(e,t){var n=this.props.prefixCls;if(t.current!==this.state.current&&this.paginationNode){var r=this.paginationNode.querySelector("."+n+"-item-"+t.current);r&&document.activeElement===r&&r.blur()}}},{key:"getValidValue",value:function(e){var t=e.target.value,n=Ea(void 0,this.state,this.props),r=this.state.currentInputValue;return""===t?t:isNaN(Number(t))?r:t>=n?n:Number(t)}},{key:"render",value:function(){var e=this.props,t=e.prefixCls,n=e.className,r=e.disabled;if(!0===this.props.hideOnSinglePage&&this.props.total<=this.state.pageSize)return null;var c=this.props,i=c.locale,a=Ea(void 0,this.state,this.props),l=[],u=null,s=null,f=null,p=null,h=null,d=c.showQuickJumper&&c.showQuickJumper.goButton,v=c.showLessItems?1:2,m=this.state,y=m.current,b=m.pageSize,g=y-1>0?y-1:0,w=y+1=2*v&&3!==y&&(l[0]=o.a.cloneElement(l[0],{className:t+"-item-after-jump-prev"}),l.unshift(u)),a-y>=2*v&&y!==a-2&&(l[l.length-1]=o.a.cloneElement(l[l.length-1],{className:t+"-item-before-jump-next"}),l.push(s)),1!==E&&l.unshift(f),P!==a&&l.push(p)}var L=null;c.showTotal&&(L=o.a.createElement("li",{className:t+"-total-text"},c.showTotal(c.total,[0===c.total?0:(y-1)*b+1,y*b>c.total?c.total:y*b])));var j=!this.hasPrev()||!a,N=!this.hasNext()||!a;return o.a.createElement("ul",ne()({className:z()(t,n,ee()({},t+"-disabled",r)),style:c.style,unselectable:"unselectable",ref:this.savePaginationNode},O),L,o.a.createElement("li",{title:c.showTitle?i.prev_page:null,onClick:this.prev,tabIndex:j?null:0,onKeyPress:this.runIfEnterPrev,className:(j?t+"-disabled":"")+" "+t+"-prev","aria-disabled":j},c.itemRender(g,"prev",this.getItemIcon(c.prevIcon))),l,o.a.createElement("li",{title:c.showTitle?i.next_page:null,onClick:this.next,tabIndex:N?null:0,onKeyPress:this.runIfEnterNext,className:(N?t+"-disabled":"")+" "+t+"-next","aria-disabled":N},c.itemRender(w,"next",this.getItemIcon(c.nextIcon))),o.a.createElement(xa,{disabled:r,locale:c.locale,rootPrefixCls:t,selectComponentClass:c.selectComponentClass,selectPrefixCls:c.selectPrefixCls,changeSize:this.props.showSizeChanger?this.changePageSize:null,current:this.state.current,pageSize:this.state.pageSize,pageSizeOptions:this.props.pageSizeOptions,quickGo:this.shouldDisplayQuickJumper()?this.handleChange:null,goButton:d}))}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n={};if("current"in e&&(n.current=e.current,e.current!==t.current&&(n.currentInputValue=n.current)),"pageSize"in e&&e.pageSize!==t.pageSize){var r=t.current,o=Ea(e.pageSize,t,e);r=r>o?o:r,"current"in e||(n.current=r,n.currentInputValue=r),n.pageSize=e.pageSize}return n}}]),t}(o.a.Component);Pa.propTypes={disabled:u.a.bool,prefixCls:u.a.string,className:u.a.string,current:u.a.number,defaultCurrent:u.a.number,total:u.a.number,pageSize:u.a.number,defaultPageSize:u.a.number,onChange:u.a.func,hideOnSinglePage:u.a.bool,showSizeChanger:u.a.bool,showLessItems:u.a.bool,onShowSizeChange:u.a.func,selectComponentClass:u.a.func,showPrevNextJumpers:u.a.bool,showQuickJumper:u.a.oneOfType([u.a.bool,u.a.object]),showTitle:u.a.bool,pageSizeOptions:u.a.arrayOf(u.a.string),showTotal:u.a.func,locale:u.a.object,style:u.a.object,itemRender:u.a.func,prevIcon:u.a.oneOfType([u.a.func,u.a.node]),nextIcon:u.a.oneOfType([u.a.func,u.a.node]),jumpPrevIcon:u.a.oneOfType([u.a.func,u.a.node]),jumpNextIcon:u.a.oneOfType([u.a.func,u.a.node])},Pa.defaultProps={defaultCurrent:1,total:0,defaultPageSize:10,onChange:Ha,className:"",selectPrefixCls:"rc-select",prefixCls:"rc-pagination",selectComponentClass:null,hideOnSinglePage:!1,showPrevNextJumpers:!0,showQuickJumper:!1,showSizeChanger:!1,showLessItems:!1,showTitle:!0,onShowSizeChange:Ha,locale:ka.a,style:{},itemRender:function(e,t,n){return n}};var Va=function(){var e=this;this.getJumpPrevPage=function(){return Math.max(1,e.state.current-(e.props.showLessItems?3:5))},this.getJumpNextPage=function(){return Math.min(Ea(void 0,e.state,e.props),e.state.current+(e.props.showLessItems?3:5))},this.getItemIcon=function(t){var n=e.props.prefixCls,r=t||o.a.createElement("a",{className:n+"-item-link"});return"function"===typeof t&&(r=o.a.createElement(t,ne()({},e.props))),r},this.savePaginationNode=function(t){e.paginationNode=t},this.isValid=function(t){return"number"===typeof(n=t)&&isFinite(n)&&Math.floor(n)===n&&t!==e.state.current;var n},this.shouldDisplayQuickJumper=function(){var t=e.props,n=t.showQuickJumper,r=t.pageSize;return!(t.total<=r)&&n},this.handleKeyDown=function(e){e.keyCode!==Ma&&e.keyCode!==Sa||e.preventDefault()},this.handleKeyUp=function(t){var n=e.getValidValue(t);n!==e.state.currentInputValue&&e.setState({currentInputValue:n}),t.keyCode===Ca?e.handleChange(n):t.keyCode===Ma?e.handleChange(n-1):t.keyCode===Sa&&e.handleChange(n+1)},this.changePageSize=function(t){var n=e.state.current,r=Ea(t,e.state,e.props);n=n>r?r:n,0===r&&(n=e.state.current),"number"===typeof t&&("pageSize"in e.props||e.setState({pageSize:t}),"current"in e.props||e.setState({current:n,currentInputValue:n})),e.props.onShowSizeChange(n,t)},this.handleChange=function(t){var n=e.props.disabled,r=t;if(e.isValid(r)&&!n){var o=Ea(void 0,e.state,e.props);r>o?r=o:r<1&&(r=1),"current"in e.props||e.setState({current:r,currentInputValue:r});var c=e.state.pageSize;return e.props.onChange(r,c),r}return e.state.current},this.prev=function(){e.hasPrev()&&e.handleChange(e.state.current-1)},this.next=function(){e.hasNext()&&e.handleChange(e.state.current+1)},this.jumpPrev=function(){e.handleChange(e.getJumpPrevPage())},this.jumpNext=function(){e.handleChange(e.getJumpNextPage())},this.hasPrev=function(){return e.state.current>1},this.hasNext=function(){return e.state.current2?n-2:0),o=2;o-1}function bl(e,t){return function(n){e[t]=n}}function gl(){var e=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var n=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"===t?n:7&n|8).toString(16)}))}function wl(){return(wl=Object.assign||function(e){for(var t=1;t0)return!0;return!1}(r,t)){var o=n.getValueByInput(r);return void 0!==o&&n.fireChange(o),n.setOpenState(!1,{needFocus:!0}),void n.setInputValue("",!1)}n.setInputValue(r),n.setState({open:!0}),ol(n.props)&&n.fireChange([r])},n.onDropdownVisibleChange=function(e){e&&!n._focused&&(n.clearBlurTime(),n.timeoutFocus(),n._focused=!0,n.updateFocusClassName()),n.setOpenState(e)},n.onKeyDown=function(e){var t=n.state.open;if(!n.props.disabled){var r=e.keyCode;t&&!n.getInputDOMNode()?n.onInputKeyDown(e):r===b.a.ENTER||r===b.a.DOWN?(t||n.setOpenState(!0),e.preventDefault()):r===b.a.SPACE&&(t||(n.setOpenState(!0),e.preventDefault()))}},n.onInputKeyDown=function(e){var t=n.props,r=t.disabled,o=t.combobox,c=t.defaultActiveFirstOption;if(!r){var i=n.state,a=n.getRealOpenState(i),l=e.keyCode;if(!cl(n.props)||e.target.value||l!==b.a.BACKSPACE){if(l===b.a.DOWN){if(!i.open)return n.openIfHasChildren(),e.preventDefault(),void e.stopPropagation()}else if(l===b.a.ENTER&&i.open)!a&&o||e.preventDefault(),a&&o&&!1===c&&(n.comboboxTimer=setTimeout((function(){n.setOpenState(!1)})));else if(l===b.a.ESC)return void(i.open&&(n.setOpenState(!1),e.preventDefault(),e.stopPropagation()));if(a&&n.selectTriggerRef){var u=n.selectTriggerRef.getInnerMenu();u&&u.onKeyDown(e,n.handleBackfill)&&(e.preventDefault(),e.stopPropagation())}}else{e.preventDefault();var s=i.value;s.length&&n.removeSelected(s[s.length-1])}}},n.onMenuSelect=function(e){var t=e.item;if(t){var r=n.state.value,o=n.props,c=nl(t),i=r[r.length-1],a=!1;if(cl(o)?-1!==fl(r,c)?a=!0:r=r.concat([c]):ol(o)||void 0===i||i!==c||c===n.state.backfillValue?(r=[c],n.setOpenState(!1,{needFocus:!0,fireSearch:!1})):(n.setOpenState(!1,{needFocus:!0,fireSearch:!1}),a=!0),a||n.fireChange(r),n.fireSelect(c),!a){var l=ol(o)?rl(t,o.optionLabelProp):"";o.autoClearSearchValue&&n.setInputValue(l,!1)}}},n.onMenuDeselect=function(e){var t=e.item,r=e.domEvent;if("keydown"!==r.type||r.keyCode!==b.a.ENTER){var o;"click"===r.type&&n.removeSelected(nl(t)),n.props.autoClearSearchValue&&n.setInputValue("")}else{var c=v.findDOMNode(t);(o=c)&&null!==o.offsetParent&&n.removeSelected(nl(t))}},n.onArrowClick=function(e){e.stopPropagation(),e.preventDefault(),n.props.disabled||n.setOpenState(!n.state.open,{needFocus:!n.state.open})},n.onPlaceholderClick=function(){n.getInputDOMNode&&n.getInputDOMNode()&&n.getInputDOMNode().focus()},n.onOuterFocus=function(e){if(n.props.disabled)e.preventDefault();else{n.clearBlurTime();var t=n.getInputDOMNode();t&&e.target===n.rootRef||(il(n.props)||e.target!==t)&&(n._focused||(n._focused=!0,n.updateFocusClassName(),cl(n.props)&&n._mouseDown||n.timeoutFocus()))}},n.onPopupFocus=function(){n.maybeFocus(!0,!0)},n.onOuterBlur=function(e){n.props.disabled?e.preventDefault():n.blurTimer=window.setTimeout((function(){n._focused=!1,n.updateFocusClassName();var e=n.props,t=n.state.value,r=n.state.inputValue;if(al(e)&&e.showSearch&&r&&e.defaultActiveFirstOption){var o=n._options||[];if(o.length){var c=ml(o);c&&(t=[nl(c)],n.fireChange(t))}}else if(cl(e)&&r){n._mouseDown?n.setInputValue(""):(n.state.inputValue="",n.getInputDOMNode&&n.getInputDOMNode()&&(n.getInputDOMNode().value=""));var i=n.getValueByInput(r);void 0!==i&&(t=i,n.fireChange(t))}if(cl(e)&&n._mouseDown)return n.maybeFocus(!0,!0),void(n._mouseDown=!1);n.setOpenState(!1),e.onBlur&&e.onBlur(n.getVLForOnChange(t))}),10)},n.onClearSelection=function(e){var t=n.props,r=n.state;if(!t.disabled){var o=r.inputValue,c=r.value;e.stopPropagation(),(o||c.length)&&(c.length&&n.fireChange([]),n.setOpenState(!1,{needFocus:!0}),o&&n.setInputValue(""))}},n.onChoiceAnimationLeave=function(){n.forcePopupAlign()},n.getOptionInfoBySingleValue=function(e,t){var o;if((t=t||n.state.optionsInfo)[ul(e)]&&(o=t[ul(e)]),o)return o;var c=e;if(n.props.labelInValue){var i=pl(n.props.value,e),a=pl(n.props.defaultValue,e);void 0!==i?c=i:void 0!==a&&(c=a)}return{option:r.createElement(Ka,{value:e,key:e},e),value:e,label:c}},n.getOptionBySingleValue=function(e){return n.getOptionInfoBySingleValue(e).option},n.getOptionsBySingleValue=function(e){return e.map((function(e){return n.getOptionBySingleValue(e)}))},n.getValueByLabel=function(e){if(void 0===e)return null;var t=null;return Object.keys(n.state.optionsInfo).forEach((function(r){var o=n.state.optionsInfo[r];if(!o.disabled){var c=ll(o.label);c&&c.join("")===e&&(t=o.value)}})),t},n.getVLBySingleValue=function(e){return n.props.labelInValue?{key:e,label:n.getLabelBySingleValue(e)}:e},n.getVLForOnChange=function(e){var t=e;return void 0!==t?(t=n.props.labelInValue?t.map((function(e){return{key:e,label:n.getLabelBySingleValue(e)}})):t.map((function(e){return e})),cl(n.props)?t:t[0]):t},n.getLabelBySingleValue=function(e,t){return n.getOptionInfoBySingleValue(e,t).label},n.getDropdownContainer=function(){return n.dropdownContainer||(n.dropdownContainer=document.createElement("div"),document.body.appendChild(n.dropdownContainer)),n.dropdownContainer},n.getPlaceholderElement=function(){var e=n.props,t=n.state,o=!1;t.inputValue&&(o=!0);var c=t.value;c.length&&(o=!0),ol(e)&&1===c.length&&t.value&&!t.value[0]&&(o=!1);var i=e.placeholder;return i?r.createElement("div",Nl({onMouseDown:sl,style:Nl({display:o?"none":"block"},dl)},vl,{onClick:n.onPlaceholderClick,className:"".concat(e.prefixCls,"-selection__placeholder")}),i):null},n.getInputElement=function(){var e=n.props,t=r.createElement("input",{id:e.id,autoComplete:"off"}),o=e.getInputElement?e.getInputElement():t,c=z()(o.props.className,jl({},"".concat(e.prefixCls,"-search__field"),!0));return r.createElement("div",{className:"".concat(e.prefixCls,"-search__field__wrap")},r.cloneElement(o,{ref:n.saveInputRef,onChange:n.onInputChange,onKeyDown:Wl(n.onInputKeyDown,o.props.onKeyDown,n.props.onInputKeyDown),value:n.state.inputValue,disabled:e.disabled,className:c}),r.createElement("span",{ref:n.saveInputMirrorRef,className:"".concat(e.prefixCls,"-search__field__mirror")},n.state.inputValue,"\xa0"))},n.getInputDOMNode=function(){return n.topCtrlRef?n.topCtrlRef.querySelector("input,textarea,div[contentEditable]"):n.inputRef},n.getInputMirrorDOMNode=function(){return n.inputMirrorRef},n.getPopupDOMNode=function(){if(n.selectTriggerRef)return n.selectTriggerRef.getPopupDOMNode()},n.getPopupMenuComponent=function(){if(n.selectTriggerRef)return n.selectTriggerRef.getInnerMenu()},n.setOpenState=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},r=t.needFocus,o=t.fireSearch,c=n.props,i=n.state;if(i.open!==e){n.props.onDropdownVisibleChange&&n.props.onDropdownVisibleChange(e);var a={open:e,backfillValue:""};!e&&al(c)&&c.showSearch&&n.setInputValue("",o),e||n.maybeFocus(e,!!r),n.setState(Nl({open:e},a),(function(){e&&n.maybeFocus(e,!!r)}))}else n.maybeFocus(e,!!r)},n.setInputValue=function(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],r=n.props.onSearch;e!==n.state.inputValue&&n.setState((function(n){return t&&e!==n.inputValue&&r&&r(e),{inputValue:e}}),n.forcePopupAlign)},n.getValueByInput=function(e){var t=n.props,r=t.multiple,o=t.tokenSeparators,c=n.state.value,i=!1;return function(e,t){var n=new RegExp("[".concat(t.join(),"]"));return e.split(n).filter((function(e){return e}))}(e,o).forEach((function(e){var t=[e];if(r){var o=n.getValueByLabel(e);o&&-1===fl(c,o)&&(c=c.concat(o),i=!0,n.fireSelect(o))}else-1===fl(c,e)&&(c=c.concat(t),i=!0,n.fireSelect(e))})),i?c:void 0},n.getRealOpenState=function(e){var t=n.props.open;if("boolean"===typeof t)return t;var r=(e||n.state).open,o=n._options||[];return!il(n.props)&&n.props.showSearch||r&&!o.length&&(r=!1),r},n.markMouseDown=function(){n._mouseDown=!0},n.markMouseLeave=function(){n._mouseDown=!1},n.handleBackfill=function(e){if(n.props.backfill&&(al(n.props)||ol(n.props))){var t=nl(e);ol(n.props)&&n.setInputValue(t,!1),n.setState({value:[t],backfillValue:t})}},n.filterOption=function(e,t){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:yl,o=n.state.value,c=o[o.length-1];if(!e||c&&c===n.state.backfillValue)return!0;var i=n.props.filterOption;return"filterOption"in n.props?!0===i&&(i=r.bind(Al(n))):i=r.bind(Al(n)),!i||("function"===typeof i?i.call(Al(n),e,t):!t.props.disabled)},n.timeoutFocus=function(){var e=n.props.onFocus;n.focusTimer&&n.clearFocusTime(),n.focusTimer=window.setTimeout((function(){e&&e()}),10)},n.clearFocusTime=function(){n.focusTimer&&(clearTimeout(n.focusTimer),n.focusTimer=null)},n.clearBlurTime=function(){n.blurTimer&&(clearTimeout(n.blurTimer),n.blurTimer=null)},n.clearComboboxTime=function(){n.comboboxTimer&&(clearTimeout(n.comboboxTimer),n.comboboxTimer=null)},n.updateFocusClassName=function(){var e=n.rootRef,t=n.props;n._focused?$a()(e).add("".concat(t.prefixCls,"-focused")):$a()(e).remove("".concat(t.prefixCls,"-focused"))},n.maybeFocus=function(e,t){if(t||e){var r=n.getInputDOMNode(),o=document.activeElement;r&&(e||il(n.props))?o!==r&&(r.focus(),n._focused=!0):o!==n.selectionRef&&n.selectionRef&&(n.selectionRef.focus(),n._focused=!0)}},n.removeSelected=function(e,t){var r=n.props;if(!r.disabled&&!n.isChildDisabled(e)){t&&t.stopPropagation&&t.stopPropagation();var o=n.state.value.filter((function(t){return t!==e}));if(cl(r)){var c=e;r.labelInValue&&(c={key:e,label:n.getLabelBySingleValue(e)}),r.onDeselect&&r.onDeselect(c,n.getOptionBySingleValue(e))}n.fireChange(o)}},n.openIfHasChildren=function(){var e=n.props;(r.Children.count(e.children)||al(e))&&n.setOpenState(!0)},n.fireSelect=function(e){n.props.onSelect&&n.props.onSelect(n.getVLBySingleValue(e),n.getOptionBySingleValue(e))},n.fireChange=function(e){var t=n.props;"value"in t||n.setState({value:e},n.forcePopupAlign);var r=n.getVLForOnChange(e),o=n.getOptionsBySingleValue(e);t.onChange&&t.onChange(r,cl(n.props)?o:o[0])},n.isChildDisabled=function(e){return Za(n.props.children).some((function(t){return nl(t)===e&&t.props&&t.props.disabled}))},n.forcePopupAlign=function(){n.state.open&&n.selectTriggerRef&&n.selectTriggerRef.triggerRef&&n.selectTriggerRef.triggerRef.forcePopupAlign()},n.renderFilterOptions=function(){var e=n.state.inputValue,t=n.props,o=t.children,c=t.tags,i=t.notFoundContent,a=[],l=[],u=!1,s=n.renderFilterOptionsFromChildren(o,l,a);if(c){var f=n.state.value;(f=f.filter((function(t){return-1===l.indexOf(t)&&(!e||String(t).indexOf(String(e))>-1)}))).sort((function(e,t){return e.length-t.length})),f.forEach((function(e){var t=e,n=r.createElement(Gt,{style:dl,role:"option",attribute:vl,value:t,key:t},t);s.push(n),a.push(n)})),e&&a.every((function(t){return nl(t)!==e}))&&s.unshift(r.createElement(Gt,{style:dl,role:"option",attribute:vl,value:e,key:e},e))}return!s.length&&i&&(u=!0,s=[r.createElement(Gt,{style:dl,attribute:vl,disabled:!0,role:"option",value:"NOT_FOUND",key:"NOT_FOUND"},i)]),{empty:u,options:s}},n.renderFilterOptionsFromChildren=function(e,t,o){var c=[],i=n.props,a=n.state.inputValue,l=i.tags;return r.Children.forEach(e,(function(e){if(e){var i=e.type;if(i.isSelectOptGroup){var u=e.props.label,s=e.key;if(s||"string"!==typeof u?!u&&s&&(u=s):s=u,a&&n.filterOption(a,e)){var f=Za(e.props.children).map((function(e){var t=nl(e)||e.key;return r.createElement(Gt,Nl({key:t,value:t},e.props))}));c.push(r.createElement(rn,{key:s,title:u},f))}else{var p=n.renderFilterOptionsFromChildren(e.props.children,t,o);p.length&&c.push(r.createElement(rn,{key:s,title:u},p))}}else{el()(i.isSelectOption,"the children of `Select` should be `Select.Option` or `Select.OptGroup`, "+"instead of `".concat(i.name||i.displayName||e.type,"`."));var h=nl(e);if(function(e,t){if(!al(t)&&!function(e){return e.multiple}(t)&&"string"!==typeof e)throw new Error("Invalid `value` of type `".concat(typeof e,"` supplied to Option, ")+"expected `string` when `tags/combobox` is `true`.")}(h,n.props),n.filterOption(a,e)){var d=r.createElement(Gt,Nl({style:dl,attribute:vl,value:h,key:h,role:"option"},e.props));c.push(d),o.push(d)}l&&t.push(h)}}})),c},n.renderTopControlNode=function(){var e=n.state,t=e.open,o=e.inputValue,c=n.state.value,i=n.props,a=i.choiceTransitionName,l=i.prefixCls,u=i.maxTagTextLength,s=i.maxTagCount,f=i.showSearch,p=i.removeIcon,h=i.maxTagPlaceholder,d="".concat(l,"-selection__rendered"),v=null;if(al(i)){var m=null;if(c.length){var y=!1,b=1;f&&t?(y=!o)&&(b=.4):y=!0;var g=c[0],w=n.getOptionInfoBySingleValue(g),z=w.label,O=w.title;m=r.createElement("div",{key:"value",className:"".concat(l,"-selection-selected-value"),title:tl(O||z),style:{display:y?"block":"none",opacity:b}},z)}v=f?[m,r.createElement("div",{className:"".concat(l,"-search ").concat(l,"-search--inline"),key:"input",style:{display:t?"block":"none"}},n.getInputElement())]:[m]}else{var C,M=[],S=c;if(void 0!==s&&c.length>s){S=S.slice(0,s);var _=n.getVLForOnChange(c.slice(s,c.length)),x="+ ".concat(c.length-s," ...");h&&(x="function"===typeof h?h(_):h),C=r.createElement("li",Nl({style:dl},vl,{role:"presentation",onMouseDown:sl,className:"".concat(l,"-selection__choice ").concat(l,"-selection__choice__disabled"),key:"maxTagPlaceholder",title:tl(x)}),r.createElement("div",{className:"".concat(l,"-selection__choice__content")},x))}cl(i)&&(M=S.map((function(e){var t=n.getOptionInfoBySingleValue(e),o=t.label,c=t.title||o;u&&"string"===typeof o&&o.length>u&&(o="".concat(o.slice(0,u),"..."));var i=n.isChildDisabled(e),a=i?"".concat(l,"-selection__choice ").concat(l,"-selection__choice__disabled"):"".concat(l,"-selection__choice");return r.createElement("li",Nl({style:dl},vl,{onMouseDown:sl,className:a,role:"presentation",key:e||"RC_SELECT_EMPTY_VALUE_KEY",title:tl(c)}),r.createElement("div",{className:"".concat(l,"-selection__choice__content")},o),i?null:r.createElement("span",{onClick:function(t){n.removeSelected(e,t)},className:"".concat(l,"-selection__choice__remove")},p||r.createElement("i",{className:"".concat(l,"-selection__choice__remove-icon")},"\xd7")))}))),C&&M.push(C),M.push(r.createElement("li",{className:"".concat(l,"-search ").concat(l,"-search--inline"),key:"__input"},n.getInputElement())),v=cl(i)&&a?r.createElement(Qa.a,{onLeave:n.onChoiceAnimationLeave,component:"ul",transitionName:a},M):r.createElement("ul",null,M)}return r.createElement("div",{className:d,ref:n.saveTopCtrlRef},n.getPlaceholderElement(),v)};var o=t.getOptionsInfoFromProps(e);if(e.tags&&"function"!==typeof e.filterOption){var c=Object.keys(o).some((function(e){return o[e].disabled}));el()(!c,"Please avoid setting option to disabled in tags mode since user can always type text as tag.")}return n.state={value:t.getValueFromProps(e,!0),inputValue:e.combobox?t.getInputValueForCombobox(e,o,!0):"",open:e.defaultOpen,optionsInfo:o,backfillValue:"",skipBuildOptionsInfo:!0,ariaId:""},n.saveInputRef=bl(Al(n),"inputRef"),n.saveInputMirrorRef=bl(Al(n),"inputMirrorRef"),n.saveTopCtrlRef=bl(Al(n),"topCtrlRef"),n.saveSelectTriggerRef=bl(Al(n),"selectTriggerRef"),n.saveRootRef=bl(Al(n),"rootRef"),n.saveSelectionRef=bl(Al(n),"selectionRef"),n}var n,o,c;return function(e,t){if("function"!==typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function");e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,writable:!0,configurable:!0}}),t&&Il(e,t)}(t,e),n=t,(o=[{key:"componentDidMount",value:function(){(this.props.autoFocus||this.state.open)&&this.focus(),this.setState({ariaId:gl()})}},{key:"componentDidUpdate",value:function(){if(cl(this.props)){var e=this.getInputDOMNode(),t=this.getInputMirrorDOMNode();e&&e.value&&t?(e.style.width="",e.style.width="".concat(t.clientWidth,"px")):e&&(e.style.width="")}this.forcePopupAlign()}},{key:"componentWillUnmount",value:function(){this.clearFocusTime(),this.clearBlurTime(),this.clearComboboxTime(),this.dropdownContainer&&(v.unmountComponentAtNode(this.dropdownContainer),document.body.removeChild(this.dropdownContainer),this.dropdownContainer=null)}},{key:"focus",value:function(){al(this.props)&&this.selectionRef?this.selectionRef.focus():this.getInputDOMNode()&&this.getInputDOMNode().focus()}},{key:"blur",value:function(){al(this.props)&&this.selectionRef?this.selectionRef.blur():this.getInputDOMNode()&&this.getInputDOMNode().blur()}},{key:"renderArrow",value:function(e){var t=this.props,n=t.showArrow,o=void 0===n?!e:n,c=t.loading,i=t.inputIcon,a=t.prefixCls;if(!o&&!c)return null;var l=c?r.createElement("i",{className:"".concat(a,"-arrow-loading")}):r.createElement("i",{className:"".concat(a,"-arrow-icon")});return r.createElement("span",Nl({key:"arrow",className:"".concat(a,"-arrow"),style:dl},vl,{onClick:this.onArrowClick}),i||l)}},{key:"renderClear",value:function(){var e=this.props,t=e.prefixCls,n=e.allowClear,o=e.clearIcon,c=this.state.inputValue,i=this.state.value,a=r.createElement("span",Nl({key:"clear",className:"".concat(t,"-selection__clear"),onMouseDown:sl,style:dl},vl,{onClick:this.onClearSelection}),o||r.createElement("i",{className:"".concat(t,"-selection__clear-icon")},"\xd7"));return n?ol(this.props)?c?a:null:c||i.length?a:null:null}},{key:"render",value:function(){var e,t=this.props,n=cl(t),o=t.showArrow,c=void 0===o||o,i=this.state,a=t.className,l=t.disabled,u=t.prefixCls,s=t.loading,f=this.renderTopControlNode(),p=this.state,h=p.open,d=p.ariaId;if(h){var v=this.renderFilterOptions();this._empty=v.empty,this._options=v.options}var m=this.getRealOpenState(),y=this._empty,b=this._options||[],g={};Object.keys(t).forEach((function(e){!Object.prototype.hasOwnProperty.call(t,e)||"data-"!==e.substr(0,5)&&"aria-"!==e.substr(0,5)&&"role"!==e||(g[e]=t[e])}));var w=Nl({},g);il(t)||(w=Nl(Nl({},w),{onKeyDown:this.onKeyDown,tabIndex:t.disabled?-1:t.tabIndex}));var O=(jl(e={},a,!!a),jl(e,u,1),jl(e,"".concat(u,"-open"),h),jl(e,"".concat(u,"-focused"),h||!!this._focused),jl(e,"".concat(u,"-combobox"),ol(t)),jl(e,"".concat(u,"-disabled"),l),jl(e,"".concat(u,"-enabled"),!l),jl(e,"".concat(u,"-allow-clear"),!!t.allowClear),jl(e,"".concat(u,"-no-arrow"),!c),jl(e,"".concat(u,"-loading"),!!s),e);return r.createElement(Ll,{onPopupFocus:this.onPopupFocus,onMouseEnter:this.props.onMouseEnter,onMouseLeave:this.props.onMouseLeave,dropdownAlign:t.dropdownAlign,dropdownClassName:t.dropdownClassName,dropdownMatchSelectWidth:t.dropdownMatchSelectWidth,defaultActiveFirstOption:t.defaultActiveFirstOption,dropdownMenuStyle:t.dropdownMenuStyle,transitionName:t.transitionName,animation:t.animation,prefixCls:t.prefixCls,dropdownStyle:t.dropdownStyle,combobox:t.combobox,showSearch:t.showSearch,options:b,empty:y,multiple:n,disabled:l,visible:m,inputValue:i.inputValue,value:i.value,backfillValue:i.backfillValue,firstActiveValue:t.firstActiveValue,onDropdownVisibleChange:this.onDropdownVisibleChange,getPopupContainer:t.getPopupContainer,onMenuSelect:this.onMenuSelect,onMenuDeselect:this.onMenuDeselect,onPopupScroll:t.onPopupScroll,showAction:t.showAction,ref:this.saveSelectTriggerRef,menuItemSelectedIcon:t.menuItemSelectedIcon,dropdownRender:t.dropdownRender,ariaId:d},r.createElement("div",{id:t.id,style:t.style,ref:this.saveRootRef,onBlur:this.onOuterBlur,onFocus:this.onOuterFocus,className:z()(O),onMouseDown:this.markMouseDown,onMouseUp:this.markMouseLeave,onMouseOut:this.markMouseLeave},r.createElement("div",Nl({ref:this.saveSelectionRef,key:"selection",className:"".concat(u,"-selection\n ").concat(u,"-selection--").concat(n?"multiple":"single"),role:"combobox","aria-autocomplete":"list","aria-haspopup":"true","aria-controls":d,"aria-expanded":m},w),f,this.renderClear(),this.renderArrow(!!n))))}}])&&Dl(n.prototype,o),c&&Dl(n,c),t}(r.Component);Ul.propTypes=qa,Ul.defaultProps={prefixCls:"rc-select",defaultOpen:!1,labelInValue:!1,defaultActiveFirstOption:!0,showSearch:!0,allowClear:!1,placeholder:"",onChange:Fl,onFocus:Fl,onBlur:Fl,onSelect:Fl,onSearch:Fl,onDeselect:Fl,onInputKeyDown:Fl,dropdownMatchSelectWidth:!0,dropdownStyle:{},dropdownMenuStyle:{},optionFilterProp:"value",optionLabelProp:"value",notFoundContent:"Not Found",backfill:!1,showAction:["click"],tokenSeparators:[],autoClearSearchValue:!0,tabIndex:0,dropdownRender:function(e){return e}},Ul.getDerivedStateFromProps=function(e,t){var n=t.skipBuildOptionsInfo?t.optionsInfo:Ul.getOptionsInfoFromProps(e,t),r={optionsInfo:n,skipBuildOptionsInfo:!1};if("open"in e&&(r.open=e.open),e.disabled&&t.open&&(r.open=!1),"value"in e){var o=Ul.getValueFromProps(e);r.value=o,e.combobox&&(r.inputValue=Ul.getInputValueForCombobox(e,n))}return r},Ul.getOptionsFromChildren=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:[];return r.Children.forEach(e,(function(e){e&&(e.type.isSelectOptGroup?Ul.getOptionsFromChildren(e.props.children,t):t.push(e))})),t},Ul.getInputValueForCombobox=function(e,t,n){var r=[];if("value"in e&&!n&&(r=ll(e.value)),"defaultValue"in e&&n&&(r=ll(e.defaultValue)),!r.length)return"";var o=r=r[0];return e.labelInValue?o=r.label:t[ul(r)]&&(o=t[ul(r)].label),void 0===o&&(o=""),o},Ul.getLabelFromOption=function(e,t){return rl(t,e.optionLabelProp)},Ul.getOptionsInfoFromProps=function(e,t){var n=Ul.getOptionsFromChildren(e.children),r={};if(n.forEach((function(t){var n=nl(t);r[ul(n)]={option:t,value:n,label:Ul.getLabelFromOption(e,t),title:t.props.title,disabled:t.props.disabled}})),t){var o=t.optionsInfo,c=t.value;c&&c.forEach((function(e){var t=ul(e);r[t]||void 0===o[t]||(r[t]=o[t])}))}return r},Ul.getValueFromProps=function(e,t){var n=[];return"value"in e&&!t&&(n=ll(e.value)),"defaultValue"in e&&t&&(n=ll(e.defaultValue)),e.labelInValue&&(n=n.map((function(e){return e.key}))),n},Ul.displayName="Select",Object(d.polyfill)(Ul);var Kl=Ul;Kl.Option=Ka,Kl.OptGroup=Aa;var Bl=Kl;function Yl(e){return(Yl="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function ql(){return(ql=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{},t=e&&e.body&&e.body.row;return as(as({},e),{body:as(as({},e.body),{row:ba(t)})})};function ys(e,t){return Wo(t||(e||{}).columns||[],(function(e){return"undefined"!==typeof e.filteredValue}))}function bs(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1?arguments[1]:void 0,n={};return ys(e,t).forEach((function(e){var t=ps(e);n[t]=e.filteredValue})),n}var gs=function(e){ns(n,e);var t=os(n);function n(e){var o;Ju(this,n),(o=t.call(this,e)).setTableRef=function(e){o.rcTable=e},o.getCheckboxPropsByItem=function(e,t){var n=fs(o.props);if(!n.getCheckboxProps)return{};var r=o.getRecordKey(e,t);if(!o.props.checkboxPropsCache[r]){o.props.checkboxPropsCache[r]=n.getCheckboxProps(e)||{};var c=o.props.checkboxPropsCache[r];Object(bn.a)(!("checked"in c)&&!("defaultChecked"in c),"Table","Do not set `checked` or `defaultChecked` in `getCheckboxProps`. Please use `selectedRowKeys` instead.")}return o.props.checkboxPropsCache[r]},o.getRecordKey=function(e,t){var n=o.props.rowKey,r="function"===typeof n?n(e,t):e[n];return Object(bn.a)(void 0!==r,"Table","Each record in dataSource of table should have a unique `key` prop, or set `rowKey` of Table to an unique primary key, see https://u.ant.design/table-row-key"),void 0===r?t:r},o.onRow=function(e,t,n){var r=o.props.onRow;return as(as({},r?r(t,n):{}),{prefixCls:e,store:o.props.store,rowKey:o.getRecordKey(t,n)})},o.generatePopupContainerFunc=function(e){var t=o.props.scroll,n=o.rcTable;return e||(t&&n?function(){return n.tableNode}:void 0)},o.scrollToFirstRow=function(){var e=o.props.scroll;e&&!1!==e.scrollToFirstRowOnChange&&function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.getContainer,r=void 0===n?function(){return window}:n,o=t.callback,c=t.duration,i=void 0===c?450:c,a=r(),l=ga(a,!0),u=Date.now(),s=function t(){var n=Date.now()-u,r=wa(n>i?i:n,l,e,i);a===window?window.scrollTo(window.pageXOffset,r):a.scrollTop=r,n0&&(a.filters=l),"object"===Xu(n.pagination)&&"current"in n.pagination&&(a.pagination=as(as({},r),{current:o.state.pagination.current})),o.setState(a,(function(){o.scrollToFirstRow(),o.props.store.setState({selectionDirty:!1});var e=o.props.onChange;e&&e.apply(null,o.prepareParamsArguments(as(as({},o.state),{selectionDirty:!1,filters:c,pagination:r})))}))},o.handleSelect=function(e,t,n){var r=n.target.checked,c=n.nativeEvent,i=o.props.store.getState().selectionDirty?[]:o.getDefaultSelection(),a=o.props.store.getState().selectedRowKeys.concat(i),l=o.getRecordKey(e,t),u=o.state.pivot,s=o.getFlatCurrentPageData(),f=t;if(o.props.expandedRowRender&&(f=s.findIndex((function(e){return o.getRecordKey(e,t)===l}))),c.shiftKey&&void 0!==u&&f!==u){for(var p=[],h=Math.sign(u-f),d=Math.abs(u-f),v=0,m=function(){var e=f+v*h;v+=1;var t=s[e],n=o.getRecordKey(t,e);o.getCheckboxPropsByItem(t,e).disabled||(a.includes(n)?r||(a=a.filter((function(e){return n!==e})),p.push(n)):r&&(a.push(n),p.push(n)))};v<=d;)m();o.setState({pivot:f}),o.props.store.setState({selectionDirty:!0}),o.setSelectedRowKeys(a,{selectWay:"onSelectMultiple",record:e,checked:r,changeRowKeys:p,nativeEvent:c})}else r?a.push(o.getRecordKey(e,f)):a=a.filter((function(e){return l!==e})),o.setState({pivot:f}),o.props.store.setState({selectionDirty:!0}),o.setSelectedRowKeys(a,{selectWay:"onSelect",record:e,checked:r,changeRowKeys:void 0,nativeEvent:c})},o.handleRadioSelect=function(e,t,n){var r=n.target.checked,c=n.nativeEvent,i=[o.getRecordKey(e,t)];o.props.store.setState({selectionDirty:!0}),o.setSelectedRowKeys(i,{selectWay:"onSelect",record:e,checked:r,changeRowKeys:void 0,nativeEvent:c})},o.handleSelectRow=function(e,t,n){var r,c=o.getFlatCurrentPageData(),i=o.props.store.getState().selectionDirty?[]:o.getDefaultSelection(),a=o.props.store.getState().selectedRowKeys.concat(i),l=c.filter((function(e,t){return!o.getCheckboxPropsByItem(e,t).disabled})).map((function(e,t){return o.getRecordKey(e,t)})),u=[],s="onSelectAll";switch(e){case"all":l.forEach((function(e){a.indexOf(e)<0&&(a.push(e),u.push(e))})),s="onSelectAll",r=!0;break;case"removeAll":l.forEach((function(e){a.indexOf(e)>=0&&(a.splice(a.indexOf(e),1),u.push(e))})),s="onSelectAll",r=!1;break;case"invert":l.forEach((function(e){a.indexOf(e)<0?a.push(e):a.splice(a.indexOf(e),1),u.push(e),s="onSelectInvert"}))}o.props.store.setState({selectionDirty:!0});var f=o.props.rowSelection,p=2;if(f&&f.hideDefaultSelections&&(p=0),t>=p&&"function"===typeof n)return n(l);o.setSelectedRowKeys(a,{selectWay:s,checked:r,changeRowKeys:u})},o.handlePageChange=function(e){var t=o.props,n=as({},o.state.pagination);n.current=e||(n.current||1);for(var r=arguments.length,c=new Array(r>1?r-1:0),i=1;i0){var o=this.getSortStateFromColumns(t);hs(o.sortColumn,n)&&o.sortOrder===r||this.setState(o)}}},{key:"getDefaultSelection",value:function(){var e=this;return fs(this.props).getCheckboxProps?this.getFlatData().filter((function(t,n){return e.getCheckboxPropsByItem(t,n).defaultChecked})).map((function(t,n){return e.getRecordKey(t,n)})):[]}},{key:"getDefaultPagination",value:function(e){var t,n,r="object"===Xu(e.pagination)?e.pagination:{};return"current"in r?t=r.current:"defaultCurrent"in r&&(t=r.defaultCurrent),"pageSize"in r?n=r.pageSize:"defaultPageSize"in r&&(n=r.defaultPageSize),this.hasPagination(e)?as(as(as({},ds),r),{current:t||1,pageSize:n||10}):{}}},{key:"getSortOrderColumns",value:function(e){return Wo(e||(this.state||{}).columns||[],(function(e){return"sortOrder"in e}))}},{key:"getDefaultFilters",value:function(e){var t=bs(this.state,e);return as(as({},Wo(e||[],(function(e){return"undefined"!==typeof e.defaultFilteredValue})).reduce((function(e,t){return e[ps(t)]=t.defaultFilteredValue,e}),{})),t)}},{key:"getDefaultSortOrder",value:function(e){var t=this.getSortStateFromColumns(e),n=Wo(e||[],(function(e){return null!=e.defaultSortOrder}))[0];return n&&!t.sortColumn?{sortColumn:n,sortOrder:n.defaultSortOrder}:t}},{key:"getSortStateFromColumns",value:function(e){var t=this.getSortOrderColumns(e).filter((function(e){return e.sortOrder}))[0];return t?{sortColumn:t,sortOrder:t.sortOrder}:{sortColumn:null,sortOrder:null}}},{key:"getMaxCurrent",value:function(e){var t=this.state.pagination,n=t.current,r=t.pageSize;return(n-1)*r>=e?Math.floor((e-1)/r)+1:n}},{key:"getSorterFn",value:function(e){var t=e||this.state,n=t.sortOrder,r=t.sortColumn;if(n&&r&&"function"===typeof r.sorter)return function(e,t){var o=r.sorter(e,t,n);return 0!==o?"descend"===n?-o:o:0}}},{key:"getCurrentPageData",value:function(){var e,t,n=this.getLocalData(),r=this.state;return this.hasPagination()?(t=r.pagination.pageSize,e=this.getMaxCurrent(r.pagination.total||n.length)):(t=Number.MAX_VALUE,e=1),(n.length>t||t===Number.MAX_VALUE)&&(n=n.slice((e-1)*t,e*t)),n}},{key:"getFlatData",value:function(){var e=this.props.childrenColumnName;return Io(this.getLocalData(null,!1),e)}},{key:"getFlatCurrentPageData",value:function(){var e=this.props.childrenColumnName;return Io(this.getCurrentPageData(),e)}},{key:"getLocalData",value:function(e){var t=this,n=!(arguments.length>1&&void 0!==arguments[1])||arguments[1],r=e||this.state,o=this.props.dataSource,c=o||[];c=c.slice(0);var i=this.getSorterFn(r);return i&&(c=this.recursiveSort(c,i)),n&&r.filters&&Object.keys(r.filters).forEach((function(e){var n=t.findColumn(e);if(n){var o=r.filters[e]||[];if(0!==o.length){var i=n.onFilter;c=i?c.filter((function(e){return o.some((function(t){return i(t,e)}))})):c}}})),c}},{key:"setSelectedRowKeys",value:function(e,t){var n=this,r=t.selectWay,o=t.record,c=t.checked,i=t.changeRowKeys,a=t.nativeEvent,l=fs(this.props);l&&!("selectedRowKeys"in l)&&this.props.store.setState({selectedRowKeys:e});var u=this.getFlatData();if(l.onChange||l[r]){var s=u.filter((function(t,r){return e.indexOf(n.getRecordKey(t,r))>=0}));if(l.onChange&&l.onChange(e,s),"onSelect"===r&&l.onSelect)l.onSelect(o,c,s,a);else if("onSelectMultiple"===r&&l.onSelectMultiple){var f=u.filter((function(e,t){return i.indexOf(n.getRecordKey(e,t))>=0}));l.onSelectMultiple(c,s,f)}else if("onSelectAll"===r&&l.onSelectAll){var p=u.filter((function(e,t){return i.indexOf(n.getRecordKey(e,t))>=0}));l.onSelectAll(c,s,p)}else"onSelectInvert"===r&&l.onSelectInvert&&l.onSelectInvert(e)}}},{key:"toggleSortOrder",value:function(e){var t,n=e.sortDirections||this.props.sortDirections,r=this.state,o=r.sortOrder;if(hs(r.sortColumn,e)&&void 0!==o){var c=n.indexOf(o)+1;t=c===n.length?void 0:n[c]}else t=n[0];var i={sortOrder:t,sortColumn:t?e:null};0===this.getSortOrderColumns().length&&this.setState(i,this.scrollToFirstRow);var a=this.props.onChange;a&&a.apply(null,this.prepareParamsArguments(as(as({},this.state),i),e))}},{key:"hasPagination",value:function(e){return!1!==(e||this.props).pagination}},{key:"isSortColumn",value:function(e){var t=this.state.sortColumn;return!(!e||!t)&&ps(t)===ps(e)}},{key:"prepareParamsArguments",value:function(e,t){var n=as({},e.pagination);delete n.onChange,delete n.onShowSizeChange;var r=e.filters,o={},c=t;return e.sortColumn&&e.sortOrder&&(c=e.sortColumn,o.column=e.sortColumn,o.order=e.sortOrder),c&&(o.field=c.dataIndex,o.columnKey=ps(c)),[n,r,o,{currentDataSource:this.getLocalData(e)}]}},{key:"findColumn",value:function(e){var t;return Fo(this.state.columns,(function(n){ps(n)===e&&(t=n)})),t}},{key:"recursiveSort",value:function(e,t){var n=this,r=this.props.childrenColumnName,o=void 0===r?"children":r;return e.sort(t).map((function(e){return e[o]?as(as({},e),Zu({},o,n.recursiveSort(e[o],t))):e}))}},{key:"renderPagination",value:function(e,t){if(!this.hasPagination())return null;var n="default",o=this.state.pagination;o.size?n=o.size:"middle"!==this.props.size&&"small"!==this.props.size||(n="small");var c=o.position||"bottom",i=o.total||this.getLocalData().length;return i>0&&(c===t||"both"===c)?r.createElement(Cu,as({key:"pagination-".concat(t)},o,{className:f()(o.className,"".concat(e,"-pagination")),onChange:this.handlePageChange,total:i,size:n,current:this.getMaxCurrent(i),onShowSizeChange:this.handleShowSizeChange})):null}},{key:"renderRowSelection",value:function(e){var t=this,n=e.prefixCls,o=e.locale,c=e.getPopupContainer,a=this.props.rowSelection,l=this.state.columns.concat();if(a){var u=this.getFlatCurrentPageData().filter((function(e,n){return!a.getCheckboxProps||!t.getCheckboxPropsByItem(e,n).disabled})),s=f()("".concat(n,"-selection-column"),Zu({},"".concat(n,"-selection-column-custom"),a.selections)),p=Zu({key:"selection-column",render:this.renderSelectionBox(a.type),className:s,fixed:a.fixed,width:a.columnWidth,title:a.columnTitle},i.INTERNAL_COL_DEFINE,{className:"".concat(n,"-selection-col")});if("radio"!==a.type){var h=u.every((function(e,n){return t.getCheckboxPropsByItem(e,n).disabled}));p.title=p.title||r.createElement(Yi,{store:this.props.store,locale:o,data:u,getCheckboxPropsByItem:this.getCheckboxPropsByItem,getRecordKey:this.getRecordKey,disabled:h,prefixCls:n,onSelect:this.handleSelectRow,selections:a.selections,hideDefaultSelections:a.hideDefaultSelections,getPopupContainer:this.generatePopupContainerFunc(c)})}"fixed"in a?p.fixed=a.fixed:l.some((function(e){return"left"===e.fixed||!0===e.fixed}))&&(p.fixed="left"),l[0]&&"selection-column"===l[0].key?l[0]=p:l.unshift(p)}return l}},{key:"renderColumnsDropdown",value:function(e){var t=this,n=e.prefixCls,o=e.dropdownPrefixCls,c=e.columns,i=e.locale,a=e.getPopupContainer,l=this.state,u=l.sortOrder,s=l.filters;return Fo(c,(function(e,c){var l,p,h,d=ps(e,c),v=e.onHeaderCell,m=t.isSortColumn(e);if(e.filters&&e.filters.length>0||e.filterDropdown){var y=d in s?s[d]:[];p=r.createElement(tc,{locale:i,column:e,selectedKeys:y,confirmFilter:t.handleFilter,prefixCls:"".concat(n,"-filter"),dropdownPrefixCls:o||"ant-dropdown",getPopupContainer:t.generatePopupContainerFunc(a),key:"filter-dropdown"})}if(e.sorter){var b=e.sortDirections||t.props.sortDirections,g=m&&"ascend"===u,w=m&&"descend"===u,z=-1!==b.indexOf("ascend")&&r.createElement(gn.a,{className:"".concat(n,"-column-sorter-up ").concat(g?"on":"off"),type:"caret-up",theme:"filled"}),O=-1!==b.indexOf("descend")&&r.createElement(gn.a,{className:"".concat(n,"-column-sorter-down ").concat(w?"on":"off"),type:"caret-down",theme:"filled"});h=r.createElement("div",{title:i.sortTitle,className:f()("".concat(n,"-column-sorter-inner"),z&&O&&"".concat(n,"-column-sorter-inner-full")),key:"sorter"},z,O),v=function(n){var r={};e.onHeaderCell&&(r=as({},e.onHeaderCell(n)));var o=r.onClick;return r.onClick=function(){t.toggleSortOrder(e),o&&o.apply(void 0,arguments)},r}}return as(as({},e),{className:f()(e.className,(l={},Zu(l,"".concat(n,"-column-has-actions"),h||p),Zu(l,"".concat(n,"-column-has-filters"),p),Zu(l,"".concat(n,"-column-has-sorters"),h),Zu(l,"".concat(n,"-column-sort"),m&&u),l)),title:[r.createElement("span",{key:"title",className:"".concat(n,"-header-column")},r.createElement("div",{className:h?"".concat(n,"-column-sorters"):void 0},r.createElement("span",{className:"".concat(n,"-column-title")},t.renderColumnTitle(e.title)),r.createElement("span",{className:"".concat(n,"-column-sorter")},h))),p],onHeaderCell:v})}))}},{key:"renderColumnTitle",value:function(e){var t=this.state,n=t.filters,r=t.sortOrder,o=t.sortColumn;return e instanceof Function?e({filters:n,sortOrder:r,sortColumn:o}):e}},{key:"render",value:function(){return r.createElement(yn.a,null,this.renderComponent)}}],[{key:"getDerivedStateFromProps",value:function(e,t){var n,r,o=t.prevProps,c=e.columns||Uo(e.children),i=as(as({},t),{prevProps:e,columns:c});if("pagination"in e||"pagination"in o){var a=as(as(as({},ds),t.pagination),e.pagination);a.current=a.current||1,a.pageSize=a.pageSize||10,i=as(as({},i),{pagination:!1!==e.pagination?a:vs})}if(e.rowSelection&&"selectedRowKeys"in e.rowSelection?e.store.setState({selectedRowKeys:e.rowSelection.selectedRowKeys||[]}):o.rowSelection&&!e.rowSelection&&e.store.setState({selectedRowKeys:[]}),"dataSource"in e&&e.dataSource!==o.dataSource&&e.store.setState({selectionDirty:!1}),e.setCheckboxPropsCache({}),ys(i,i.columns).length>0){var l=bs(i,i.columns),u=as({},i.filters);Object.keys(l).forEach((function(e){u[e]=l[e]})),n=i,r=u,(Object.keys(r).length!==Object.keys(n.filters).length||Object.keys(r).some((function(e){return r[e]!==n.filters[e]})))&&(i=as(as({},i),{filters:u}))}if(!function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e===t||["table","header","body"].every((function(n){return h()(e[n],t[n])}))}(e.components,o.components)){var s=ms(e.components);i=as(as({},i),{components:s})}return i}}]),n}(r.Component);gs.propTypes={dataSource:l.array,columns:l.array,prefixCls:l.string,useFixedHeader:l.bool,rowSelection:l.object,className:l.string,size:l.string,loading:l.oneOfType([l.bool,l.object]),bordered:l.bool,onChange:l.func,locale:l.object,dropdownPrefixCls:l.string,sortDirections:l.array,getPopupContainer:l.func},gs.defaultProps={dataSource:[],useFixedHeader:!1,className:"",size:"default",loading:!1,bordered:!1,indentSize:20,locale:{},rowKey:"key",showHeader:!0,sortDirections:["ascend","descend"],childrenColumnName:"children"},Object(d.polyfill)(gs);var ws=function(e){ns(n,e);var t=os(n);function n(e){var r;return Ju(this,n),(r=t.call(this,e)).setCheckboxPropsCache=function(e){return r.CheckboxPropsCache=e},r.CheckboxPropsCache={},r.store=function(e){var t=e,n=[];return{setState:function(e){t=nc(nc({},t),e);for(var r=0;r=arguments.length)?s=t[u]:(s=arguments[c],c+=1),o[u]=s,Object(i.a)(s)||(l-=1),u+=1}return l<=0?n.apply(this,o):Object(r.a)(l,a(e,o,n))}}var l=Object(c.a)((function(e,t){return 1===e?Object(o.a)(t):Object(r.a)(e,a(e,[],t))}));t.a=l},function(e,t,n){var r=n(36),o=n(37),c=n(123),i=n(49),a=n(43),l=function e(t,n,l){var u,s,f,p=t&e.F,h=t&e.G,d=t&e.S,v=t&e.P,m=t&e.B,y=t&e.W,b=h?o:o[n]||(o[n]={}),g=b.prototype,w=h?r:d?r[n]:(r[n]||{}).prototype;for(u in h&&(l=n),l)(s=!p&&w&&void 0!==w[u])&&a(b,u)||(f=s?w[u]:l[u],b[u]=h&&"function"!=typeof w[u]?l[u]:m&&s?c(f,r):y&&w[u]==f?function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t}(f):v&&"function"==typeof f?c(Function.call,f):f,v&&((b.virtual||(b.virtual={}))[u]=f,t&e.R&&g&&!g[u]&&i(g,u,f)))};l.F=1,l.G=2,l.S=4,l.P=8,l.B=16,l.W=32,l.U=64,l.R=128,e.exports=l},function(e,t,n){var r=n(42),o=n(73);e.exports=n(38)?function(e,t,n){return r.f(e,t,o(1,n))}:function(e,t,n){return e[t]=n,e}},function(e,t){e.exports=function(e){return"object"===typeof e?null!==e:"function"===typeof e}},function(e,t,n){var r=n(127),o=n(102);e.exports=function(e){return r(o(e))}},function(e,t,n){var r=n(105)("wks"),o=n(76),c=n(36).Symbol,i="function"==typeof c;(e.exports=function(e){return r[e]||(r[e]=i&&c[e]||(i?c:o)("Symbol."+e))}).store=r},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t,n){"use strict";var r,o;Object.defineProperty(t,"__esModule",{value:!0});var c={position:"absolute",top:"-9999px",width:"50px",height:"50px"};t.INTERNAL_COL_DEFINE="RC_TABLE_INTERNAL_COL_DEFINE",t.measureScrollbar=function(e){var t=e.direction,n=void 0===t?"vertical":t,i=e.prefixCls;if("undefined"===typeof document||"undefined"===typeof window)return 0;var a="vertical"===n;if(a&&r)return r;if(!a&&o)return o;var l=document.createElement("div");Object.keys(c).forEach((function(e){l.style[e]=c[e]})),l.className="".concat(i,"-hide-scrollbar scroll-div-append-to-body"),a?l.style.overflowY="scroll":l.style.overflowX="scroll",document.body.appendChild(l);var u=0;return a?(u=l.offsetWidth-l.clientWidth,r=u):(u=l.offsetHeight-l.clientHeight,o=u),document.body.removeChild(l),u},t.debounce=function(e,t,n){var r;function o(){for(var o=arguments.length,c=new Array(o),i=0;i=0&&"[object Array]"===Object.prototype.toString.call(e)}},function(e,t,n){"use strict";function r(e,t){switch(e){case 0:return function(){return t.apply(this,arguments)};case 1:return function(e){return t.apply(this,arguments)};case 2:return function(e,n){return t.apply(this,arguments)};case 3:return function(e,n,r){return t.apply(this,arguments)};case 4:return function(e,n,r,o){return t.apply(this,arguments)};case 5:return function(e,n,r,o,c){return t.apply(this,arguments)};case 6:return function(e,n,r,o,c,i){return t.apply(this,arguments)};case 7:return function(e,n,r,o,c,i,a){return t.apply(this,arguments)};case 8:return function(e,n,r,o,c,i,a,l){return t.apply(this,arguments)};case 9:return function(e,n,r,o,c,i,a,l,u){return t.apply(this,arguments)};case 10:return function(e,n,r,o,c,i,a,l,u,s){return t.apply(this,arguments)};default:throw new Error("First argument to _arity must be a non-negative integer no greater than ten")}}n.d(t,"a",(function(){return r}))},function(e,t,n){"use strict";n.d(t,"b",(function(){return g})),n.d(t,"a",(function(){return w})),n.d(t,"c",(function(){return z}));var r=n(0),o=n(29),c=n.n(o),i=n(3),a=n.n(i),l=n(30),u=function(){return r.createElement("svg",{width:"184",height:"152",viewBox:"0 0 184 152",xmlns:"http://www.w3.org/2000/svg"},r.createElement("g",{fill:"none",fillRule:"evenodd"},r.createElement("g",{transform:"translate(24 31.67)"},r.createElement("ellipse",{fillOpacity:".8",fill:"#F5F5F7",cx:"67.797",cy:"106.89",rx:"67.797",ry:"12.668"}),r.createElement("path",{d:"M122.034 69.674L98.109 40.229c-1.148-1.386-2.826-2.225-4.593-2.225h-51.44c-1.766 0-3.444.839-4.592 2.225L13.56 69.674v15.383h108.475V69.674z",fill:"#AEB8C2"}),r.createElement("path",{d:"M101.537 86.214L80.63 61.102c-1.001-1.207-2.507-1.867-4.048-1.867H31.724c-1.54 0-3.047.66-4.048 1.867L6.769 86.214v13.792h94.768V86.214z",fill:"url(#linearGradient-1)",transform:"translate(13.56)"}),r.createElement("path",{d:"M33.83 0h67.933a4 4 0 0 1 4 4v93.344a4 4 0 0 1-4 4H33.83a4 4 0 0 1-4-4V4a4 4 0 0 1 4-4z",fill:"#F5F5F7"}),r.createElement("path",{d:"M42.678 9.953h50.237a2 2 0 0 1 2 2V36.91a2 2 0 0 1-2 2H42.678a2 2 0 0 1-2-2V11.953a2 2 0 0 1 2-2zM42.94 49.767h49.713a2.262 2.262 0 1 1 0 4.524H42.94a2.262 2.262 0 0 1 0-4.524zM42.94 61.53h49.713a2.262 2.262 0 1 1 0 4.525H42.94a2.262 2.262 0 0 1 0-4.525zM121.813 105.032c-.775 3.071-3.497 5.36-6.735 5.36H20.515c-3.238 0-5.96-2.29-6.734-5.36a7.309 7.309 0 0 1-.222-1.79V69.675h26.318c2.907 0 5.25 2.448 5.25 5.42v.04c0 2.971 2.37 5.37 5.277 5.37h34.785c2.907 0 5.277-2.421 5.277-5.393V75.1c0-2.972 2.343-5.426 5.25-5.426h26.318v33.569c0 .617-.077 1.216-.221 1.789z",fill:"#DCE0E6"})),r.createElement("path",{d:"M149.121 33.292l-6.83 2.65a1 1 0 0 1-1.317-1.23l1.937-6.207c-2.589-2.944-4.109-6.534-4.109-10.408C138.802 8.102 148.92 0 161.402 0 173.881 0 184 8.102 184 18.097c0 9.995-10.118 18.097-22.599 18.097-4.528 0-8.744-1.066-12.28-2.902z",fill:"#DCE0E6"}),r.createElement("g",{transform:"translate(149.65 15.383)",fill:"#FFF"},r.createElement("ellipse",{cx:"20.654",cy:"3.167",rx:"2.849",ry:"2.815"}),r.createElement("path",{d:"M5.698 5.63H0L2.898.704zM9.259.704h4.985V5.63H9.259z"}))))},s=function(){return r.createElement("svg",{width:"64",height:"41",viewBox:"0 0 64 41",xmlns:"http://www.w3.org/2000/svg"},r.createElement("g",{transform:"translate(0 1)",fill:"none",fillRule:"evenodd"},r.createElement("ellipse",{fill:"#F5F5F5",cx:"32",cy:"33",rx:"32",ry:"7"}),r.createElement("g",{fillRule:"nonzero",stroke:"#D9D9D9"},r.createElement("path",{d:"M55 12.76L44.854 1.258C44.367.474 43.656 0 42.907 0H21.093c-.749 0-1.46.474-1.947 1.257L9 12.761V22h46v-9.24z"}),r.createElement("path",{d:"M41.613 15.931c0-1.605.994-2.93 2.227-2.931H55v18.137C55 33.26 53.68 35 52.05 35h-40.1C10.32 35 9 33.259 9 31.137V13h11.16c1.233 0 2.227 1.323 2.227 2.928v.022c0 1.605 1.005 2.901 2.237 2.901h14.752c1.232 0 2.237-1.308 2.237-2.913v-.007z",fill:"#FAFAFA"}))))};function f(){return(f=Object.assign||function(e){for(var t=1;t0&&(e.hasOwnProperty(0)&&e.hasOwnProperty(e.length-1)))))})),a=function(){function e(e){this.f=e}return e.prototype["@@transducer/init"]=function(){throw new Error("init not implemented on XWrap")},e.prototype["@@transducer/result"]=function(e){return e},e.prototype["@@transducer/step"]=function(e,t){return this.f(e,t)},e}();var l=n(58),u=n(23),s=Object(u.a)((function(e,t){return Object(l.a)(e.length,(function(){return e.apply(t,arguments)}))}));function f(e,t,n){for(var r=n.next();!r.done;){if((t=e["@@transducer/step"](t,r.value))&&t["@@transducer/reduced"]){t=t["@@transducer/value"];break}r=n.next()}return e["@@transducer/result"](t)}function p(e,t,n,r){return e["@@transducer/result"](n[r](s(e["@@transducer/step"],e),t))}var h="undefined"!==typeof Symbol?Symbol.iterator:"@@iterator";function d(e,t,n){if("function"===typeof e&&(e=function(e){return new a(e)}(e)),i(n))return function(e,t,n){for(var r=0,o=n.length;r=n&&(e.updateKey=i[0].updateKey||i[0].key,i.shift()),i.push(e)),{notices:i}}))},r.remove=function(e){r.setState((function(t){return{notices:t.notices.filter((function(t){return t.key!==e}))}}))},o=n,m()(r,o)}return b()(t,e),d()(t,[{key:"getTransitionName",value:function(){var e=this.props,t=e.transitionName;return!t&&e.animation&&(t=e.prefixCls+"-"+e.animation),t}},{key:"render",value:function(){var e,t=this,n=this.props,r=this.state.notices,c=r.map((function(e,c){var i=Boolean(c===r.length-1&&e.updateKey),a=e.updateKey?e.updateKey:e.key,l=Object(M.a)(t.remove.bind(t,e.key),e.onClose);return o.a.createElement(k,s()({prefixCls:n.prefixCls},e,{key:a,update:i,onClose:l,onClick:e.onClick,closeIcon:n.closeIcon}),e.content)})),i=(e={},l()(e,n.prefixCls,1),l()(e,n.className,!!n.className),e);return o.a.createElement("div",{className:_()(i),style:n.style},o.a.createElement(C.a,{transitionName:this.getTransitionName()},c))}}]),t}(r.Component);V.propTypes={prefixCls:w.a.string,transitionName:w.a.string,animation:w.a.oneOfType([w.a.string,w.a.object]),style:w.a.object,maxCount:w.a.number,closeIcon:w.a.node},V.defaultProps={prefixCls:"rc-notification",animation:"fade",style:{top:65,left:"50%"}},V.newInstance=function(e,t){var n=e||{},r=n.getContainer,c=i()(n,["getContainer"]),a=document.createElement("div");r?r().appendChild(a):document.body.appendChild(a);var l=!1;O.a.render(o.a.createElement(V,s()({},c,{ref:function(e){l||(l=!0,t({notice:function(t){e.add(t)},removeNotice:function(t){e.remove(t)},component:e,destroy:function(){O.a.unmountComponentAtNode(a),a.parentNode.removeChild(a)}}))}})),a)};var T=V,L=n(13);function j(){return(j=Object.assign||function(e){for(var t=1;te.length)&&(t=e.length);for(var n=0,r=new Array(t);n=0;)t=a[n],Object(o.a)(t,e)&&!u(r,t)&&(r[r.length]=t),n-=1;return r})):Object(r.a)((function(e){return Object(e)!==e?[]:Object.keys(e)}));t.a=s},function(e,t,n){"use strict";n.d(t,"a",(function(){return o}));var r=n(86);function o(e,t){return function(e){if(Array.isArray(e))return e}(e)||function(e,t){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(e)){var n=[],r=!0,o=!1,c=void 0;try{for(var i,a=e[Symbol.iterator]();!(r=(i=a.next()).done)&&(n.push(i.value),!t||n.length!==t);r=!0);}catch(l){o=!0,c=l}finally{try{r||null==a.return||a.return()}finally{if(o)throw c}}return n}}(e,t)||Object(r.a)(e,t)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}},function(e,t,n){"use strict";var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,c=Object.prototype.propertyIsEnumerable;function i(e){if(null===e||void 0===e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(o){return!1}}()?Object.assign:function(e,t){for(var n,a,l=i(e),u=1;u=t||n<0||m&&e-d>=s}function z(){var e=o();if(w(e))return O(e);p=setTimeout(z,function(e){var n=t-(e-h);return m?a(n,s-(e-d)):n}(e))}function O(e){return p=void 0,y&&l?b(e):(l=u=void 0,f)}function C(){var e=o(),n=w(e);if(l=arguments,u=this,h=e,n){if(void 0===p)return g(h);if(m)return clearTimeout(p),p=setTimeout(z,t),b(h)}return void 0===p&&(p=setTimeout(z,t)),f}return t=c(t)||0,r(n)&&(v=!!n.leading,s=(m="maxWait"in n)?i(c(n.maxWait)||0,t):s,y="trailing"in n?!!n.trailing:y),C.cancel=function(){void 0!==p&&clearTimeout(p),d=0,l=h=u=p=void 0},C.flush=function(){return void 0===p?f:O(o())},C}},function(e,t,n){"use strict";t.a={items_per_page:"\u6761/\u9875",jump_to:"\u8df3\u81f3",jump_to_confirm:"\u786e\u5b9a",page:"\u9875",prev_page:"\u4e0a\u4e00\u9875",next_page:"\u4e0b\u4e00\u9875",prev_5:"\u5411\u524d 5 \u9875",next_5:"\u5411\u540e 5 \u9875",prev_3:"\u5411\u524d 3 \u9875",next_3:"\u5411\u540e 3 \u9875"}},function(e,t,n){"use strict";(function(e){var n=function(){if("undefined"!==typeof Map)return Map;function e(e,t){var n=-1;return e.some((function(e,r){return e[0]===t&&(n=r,!0)})),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(t){var n=e(this.__entries__,t),r=this.__entries__[n];return r&&r[1]},t.prototype.set=function(t,n){var r=e(this.__entries__,t);~r?this.__entries__[r][1]=n:this.__entries__.push([t,n])},t.prototype.delete=function(t){var n=this.__entries__,r=e(n,t);~r&&n.splice(r,1)},t.prototype.has=function(t){return!!~e(this.__entries__,t)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(e,t){void 0===t&&(t=null);for(var n=0,r=this.__entries__;n0},e.prototype.connect_=function(){r&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),a?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){r&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(e){var t=e.propertyName,n=void 0===t?"":t;i.some((function(e){return!!~n.indexOf(e)}))&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),u=function(e,t){for(var n=0,r=Object.keys(t);n0},e}(),z="undefined"!==typeof WeakMap?new WeakMap:new n,O=function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=l.getInstance(),r=new w(t,n,this);z.set(this,r)};["observe","unobserve","disconnect"].forEach((function(e){O.prototype[e]=function(){var t;return(t=z.get(this))[e].apply(t,arguments)}}));var C="undefined"!==typeof o.ResizeObserver?o.ResizeObserver:O;t.a=C}).call(this,n(78))},function(e,t,n){"use strict";e.exports=n(322)},function(e,t,n){"use strict";function r(e,t){var n;t=t||[];var r=(e=e||[]).length,o=t.length,c=[];for(n=0;n=0||o.indexOf("Bottom")>=0?i.top="".concat(c.height-n.offset[1],"px"):(o.indexOf("Top")>=0||o.indexOf("bottom")>=0)&&(i.top="".concat(-n.offset[1],"px")),o.indexOf("left")>=0||o.indexOf("Right")>=0?i.left="".concat(c.width-n.offset[0],"px"):(o.indexOf("right")>=0||o.indexOf("Left")>=0)&&(i.left="".concat(-n.offset[0],"px")),e.style.transformOrigin="".concat(i.left," ").concat(i.top)}},t.renderTooltip=function(e){var n=e.getPopupContainer,o=e.getPrefixCls,c=A(t),i=c.props,a=c.state,l=i.prefixCls,u=i.openClassName,s=i.getPopupContainer,f=i.getTooltipContainer,p=i.children,h=o("tooltip",l),d=a.visible;!("visible"in i)&&t.isNoTitle()&&(d=!1);var v,m,y,b=W(r.isValidElement(p)?p:r.createElement("span",null,p)),g=b.props,w=x()(g.className,(v={},m=u||"".concat(h,"-open"),y=!0,m in v?Object.defineProperty(v,m,{value:y,enumerable:!0,configurable:!0,writable:!0}):v[m]=y,v));return r.createElement(S,F({},t.props,{prefixCls:h,getTooltipContainer:s||f||n,ref:t.saveTooltip,builtinPlacements:t.getPlacements(),overlay:t.getOverlay(),visible:d,onVisibleChange:t.onVisibleChange,onPopupAlign:t.onPopupAlign}),d?r.cloneElement(b,{className:w}):b)},t.state={visible:!!e.visible||!!e.defaultVisible},t}return t=i,o=[{key:"getDerivedStateFromProps",value:function(e){return"visible"in e?{visible:e.visible}:null}}],(n=[{key:"getPopupDomNode",value:function(){return this.tooltip.getPopupDomNode()}},{key:"getPlacements",value:function(){var e=this.props,t=e.builtinPlacements,n=e.arrowPointAtCenter,r=e.autoAdjustOverflow;return t||function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.arrowWidth,n=void 0===t?5:t,r=e.horizontalArrowShift,o=void 0===r?16:r,c=e.verticalArrowShift,i=void 0===c?12:c,a=e.autoAdjustOverflow,l=void 0===a||a,u={left:{points:["cr","cl"],offset:[-4,0]},right:{points:["cl","cr"],offset:[4,0]},top:{points:["bc","tc"],offset:[0,-4]},bottom:{points:["tc","bc"],offset:[0,4]},topLeft:{points:["bl","tc"],offset:[-(o+n),-4]},leftTop:{points:["tr","cl"],offset:[-4,-(i+n)]},topRight:{points:["br","tc"],offset:[o+n,-4]},rightTop:{points:["tl","cr"],offset:[4,-(i+n)]},bottomRight:{points:["tr","bc"],offset:[o+n,4]},rightBottom:{points:["bl","cr"],offset:[4,i+n]},bottomLeft:{points:["tl","bc"],offset:[-(o+n),4]},leftBottom:{points:["br","cl"],offset:[-4,i+n]}};return Object.keys(u).forEach((function(t){u[t]=e.arrowPointAtCenter?k(k({},u[t]),{overflow:V(l),targetOffset:P}):k(k({},z[t]),{overflow:V(l)}),u[t].ignoreShake=!0})),u}({arrowPointAtCenter:n,verticalArrowShift:8,autoAdjustOverflow:r})}},{key:"isNoTitle",value:function(){var e=this.props,t=e.title,n=e.overlay;return!t&&!n&&0!==t}},{key:"getOverlay",value:function(){var e=this.props,t=e.title,n=e.overlay;return 0===t?t:n||t||""}},{key:"render",value:function(){return r.createElement(T.a,null,this.renderTooltip)}}])&&j(t.prototype,n),o&&j(t,o),i}(r.Component);U.defaultProps={placement:"top",transitionName:"zoom-big-fast",mouseEnterDelay:.1,mouseLeaveDelay:.1,arrowPointAtCenter:!1,autoAdjustOverflow:!0},Object(c.polyfill)(U);t.a=U},function(e,t,n){"use strict";var r=n(23),o=n(57);function c(e){return null!=e&&"function"===typeof e["@@transducer/step"]}function i(e,t,n){return function(){if(0===arguments.length)return n();var r=Array.prototype.slice.call(arguments,0),i=r.pop();if(!Object(o.a)(i)){for(var a=0;a-1?n.splice(r,1):n.push(t)}e.setActiveKey(n)},this.getNewChild=function(t,n){if(!t)return null;var r=e.state.activeKey,c=e.props,i=c.prefixCls,a=c.accordion,l=c.destroyInactivePanel,u=c.expandIcon,s=t.key||String(n),f=t.props,p=f.header,h=f.headerClass,d=f.disabled,v={key:s,panelKey:s,header:p,headerClass:h,isActive:a?r[0]===s:r.indexOf(s)>-1,prefixCls:i,destroyInactivePanel:l,openAnimation:e.state.openAnimation,accordion:a,children:t.props.children,onItemClick:d?null:e.onClickItem,expandIcon:u};return"string"===typeof t.type?t:o.a.cloneElement(t,v)},this.getItems=function(){var t=e.props.children,n=Object(x.isFragment)(t)?t.props.children:t,c=r.Children.map(n,e.getNewChild);return Object(x.isFragment)(t)?o.a.createElement(o.a.Fragment,null,c):c},this.setActiveKey=function(t){"activeKey"in e.props||e.setState({activeKey:t}),e.props.onChange(e.props.accordion?t[0]:t)}};V.propTypes={children:i.a.any,prefixCls:i.a.string,activeKey:i.a.oneOfType([i.a.string,i.a.number,i.a.arrayOf(i.a.oneOfType([i.a.string,i.a.number]))]),defaultActiveKey:i.a.oneOfType([i.a.string,i.a.number,i.a.arrayOf(i.a.oneOfType([i.a.string,i.a.number]))]),openAnimation:i.a.object,onChange:i.a.func,accordion:i.a.bool,className:i.a.string,style:i.a.object,destroyInactivePanel:i.a.bool,expandIcon:i.a.func},V.defaultProps={prefixCls:"rc-collapse",onChange:function(){},accordion:!1,destroyInactivePanel:!1},V.Panel=C,Object(k.polyfill)(V);var L=V,j=(V.Panel,n(3)),N=n.n(j),D=n(59);function R(e){return(R="function"===typeof Symbol&&"symbol"===typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"===typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e})(e)}function A(){return(A=Object.assign||function(e){for(var t=1;t0&&void 0!==arguments[0]?arguments[0]:{},n=arguments.length>1?arguments[1]:void 0,o=e.props.expandIcon,c=o?o(t):r.createElement(G.a,{type:"right",rotate:t.isActive?90:void 0});return r.isValidElement(c)?r.cloneElement(c,{className:N()(c.props.className,"".concat(n,"-arrow"))}):c},e.renderCollapse=function(t){var n,o=t.getPrefixCls,c=e.props,i=c.prefixCls,a=c.className,l=void 0===a?"":a,u=c.bordered,s=c.expandIconPosition,f=o("collapse",i),p=N()((te(n={},"".concat(f,"-borderless"),!u),te(n,"".concat(f,"-icon-position-").concat(s),!0),n),l);return r.createElement(L,ee({},e.props,{expandIcon:function(t){return e.renderExpandIcon(t,f)},prefixCls:f,className:p}))},e}return t=i,(n=[{key:"render",value:function(){return r.createElement(D.a,null,this.renderCollapse)}}])&&re(t.prototype,n),o&&re(t,o),i}(r.Component);le.Panel=q,le.defaultProps={bordered:!0,openAnimation:ee(ee({},Z),{appear:function(){}}),expandIconPosition:"left"};t.a=le},function(e,t,n){var r=n(50);e.exports=function(e,t){if(!r(e))return e;var n,o;if(t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;if("function"==typeof(n=e.valueOf)&&!r(o=n.call(e)))return o;if(!t&&"function"==typeof(n=e.toString)&&!r(o=n.call(e)))return o;throw TypeError("Can't convert object to primitive value")}},function(e,t){e.exports=function(e){if(void 0==e)throw TypeError("Can't call method on "+e);return e}},function(e,t){var n=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:n)(e)}},function(e,t,n){var r=n(105)("keys"),o=n(76);e.exports=function(e){return r[e]||(r[e]=o(e))}},function(e,t,n){var r=n(37),o=n(36),c="__core-js_shared__",i=o[c]||(o[c]={});(e.exports=function(e,t){return i[e]||(i[e]=void 0!==t?t:{})})("versions",[]).push({version:r.version,mode:n(75)?"pure":"global",copyright:"\xa9 2020 Denis Pushkarev (zloirock.ru)"})},function(e,t){e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(e,t){t.f=Object.getOwnPropertySymbols},function(e,t,n){var r=n(102);e.exports=function(e){return Object(r(e))}},function(e,t){e.exports={}},function(e,t,n){var r=n(62),o=n(187),c=n(106),i=n(104)("IE_PROTO"),a=function(){},l=function(){var e,t=n(125)("iframe"),r=c.length;for(t.style.display="none",n(188).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("