提交 ce9c4cdc 编写于 作者: O ob-robot 提交者: hengtang.zj

4.1 release patch

上级 709540c9
name: Go
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.14
- name: Build
run: go build -v ./...
- name: Test
run: go test -v ./...
src/**/*.pyc
src/**/build
src/monitor_app/*.so
src/monitor_app/*.cpp
src/monitor_app/*.c
src/obagent/*.so
src/obagent/*.cpp
src/obagent/*.c
src/conf/config.conf
# go Binary files
bin/*
go.work
go.work.sum
# go test files and generated files
bindata/
iostat
tests/testdata/*
tests/testdata/data/
tests/mock*/*
tests/coverage.out
tests/coverage-report/
# macOS files
.DS_Store
.vscode
.idea
.idea/*
*.iml
*.swp
*.log
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
bin/*
......@@ -9,12 +9,14 @@
**
http://license.coscl.org.cn/MulanPSL2
http://1.1.1.1:9093
http://127.0.0.1:9093
http://127.0.0.1:9100/metrics
http://127.0.0.1:9091/metrics/node
http://127.0.0.1:9090/metrics/node
https://mirrors.aliyun.com/goproxy/
https://facebook.github.io/watchman/
https://github.com/oceanbase/obagent
https://goproxy.cn
https://github.com/krallin/tini/*
https://bixense.com/clicolors/
--------------------------------------------------------
......@@ -23,12 +25,16 @@
/README.md
/README-CN.md
/LICENCE
/plugins/inputs/oceanbase/log/log_utils.go
/plugins/inputs/prometheus/prometheus_test.go
/docs/develop-guide.md
/docs/install-and-deploy/install-obagent.md
/docs/install-and-deploy/deploy-obagent-with-obd.md
/docs/install-and-deploy/deploy-obagent-manually.md
/.git/**
/build_rpm_obagent.aci.yml
/Makefile.common
/packaging/**
**/**_test.go
/rpm/obagent.spec
--------------------------------------------------------
# Config the ignored fold to escape the Chinese scan by GLOB wildcard
# Config the ignored fold to escape the Chinese scan by GLOB wildcard
\ No newline at end of file
FROM golang:1.16 as builder
WORKDIR /workspace
RUN echo '----------- DATE_CHANGE: Add this line to disable cache during docker building -------------------'
# Copy the Go Modules manifests
RUN mkdir /workspace/obagent
ADD . /workspace/obagent
# Build
RUN cd /workspace/obagent && make build-release
FROM openanolis/anolisos:8.4-x86_64
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
WORKDIR /home/admin
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
RUN useradd -m admin
RUN mkdir -p /home/admin/obagent/bin
RUN mkdir -p /home/admin/obagent/run
RUN mkdir -p /home/admin/obagent/log
RUN mkdir -p /home/admin/obagent/conf
COPY --from=builder /workspace/obagent/bin/monagent ./obagent/bin
COPY --from=builder /workspace/obagent/etc ./obagent/etc
RUN chown -R admin:admin /home/admin/obagent
ADD ./replace_yaml.sh /home/admin/obagent
ENTRYPOINT ["/tini", "--"]
CMD ["bash", "-c", " cd /home/admin/obagent && if [ \"`ls -A conf`\" == \"\" ]; then cp -r etc/* conf/ && ./replace_yaml.sh; fi && ./bin/monagent -c conf/monagent.yaml"]
木兰宽松许可证, 第2版
2020年1月 http://license.coscl.org.cn/MulanPSL2
您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束:
0. 定义
“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。
“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。
“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。
“法人实体” 是指提交贡献的机构及其“关联实体”。
“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。
1. 授予版权许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。
2. 授予专利许可
每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。
3. 无商标许可
“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。
4. 分发限制
您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。
5. 免责声明与责任限制
“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。
6. 语言
“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。
条款结束
Mulan Permissive Software License,Version 2 (Mulan PSL v2)
January 2020 http://license.coscl.org.cn/MulanPSL2
Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions:
0. Definition
Software means the program and related documents which are licensed under this License and comprise all Contribution(s).
Contribution means the copyrightable work licensed by a particular Contributor under this License.
Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License.
Legal Entity means the entity making a Contribution and all its Affiliates.
Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity.
1. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not.
2. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken.
3. No Trademark License
No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in section 4.
4. Distribution Restriction
You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software.
5. Disclaimer of Warranty and Limitation of Liability
THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
6. Language
THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL.
END OF THE TERMS AND CONDITIONS
include Makefile.common
.PHONY: all test clean build monagent
.PHONY: pre-build build bindata mgragent monagent agentd agentctl mockgen rpm buildsucc
default: clean fmt build
default: clean fmt pre-build build
pre-build: bindata
pre-test: mockgen
bindata: get
go-bindata -o bindata/bindata.go -pkg bindata assets/...
mockgen:
mockgen -source=lib/http/http.go -destination=tests/mock/lib_http_http_mock.go -package mock
mockgen -source=lib/system/process.go -destination=tests/mock/lib_system_process_mock.go -package mock
mockgen -source=lib/system/disk.go -destination=tests/mock/lib_system_disk_mock.go -package mock
mockgen -source=lib/system/system.go -destination=tests/mock/lib_system_system_mock.go -package mock
mockgen -source=lib/shell/shell.go -destination=tests/mock/lib_shell_shell_mock.go -package mock
mockgen -source=lib/shell/command.go -destination=tests/mock/lib_shell_command_mock.go -package mock
mockgen -source=lib/shellf/shellf.go -destination=tests/mock/lib_shellf_shellf_mock.go -package mock
mockgen -source=lib/file/file.go -destination=tests/mock/lib_file_file_mock.go -package mock
mockgen -source=lib/pkg/package.go -destination=tests/mock2/lib_pkg_package_mock.go -package mock2
build: build-debug
build-debug: set-debug-flags monagent
build-debug: set-debug-flags mgragent monagent agentd agentctl buildsucc
build-release: set-release-flags mgragent monagent agentd agentctl buildsucc
build-release: set-release-flags monagent
rpm:
cd ./rpm && RELEASE=`date +%Y%m%d%H%M%S` rpmbuild -bb ./obagent.spec
set-debug-flags:
@echo Build with debug flags
$(eval LDFLAGS += $(LDFLAGS_DEBUG))
$(eval BUILD_FLAG += $(GO_RACE_FLAG))
set-release-flags:
@echo Build with release flags
$(eval LDFLAGS += $(LDFLAGS_RELEASE))
monagent:
$(GOBUILD) $(GO_RACE_FLAG) -ldflags '$(MONAGENT_LDFLAGS)' -o bin/monagent cmd/monagent/main.go
$(GO) build $(BUILD_FLAG) -ldflags '$(MONAGENT_LDFLAGS)' -o bin/ob_monagent cmd/monagent/main.go
mgragent:
$(GO) build $(BUILD_FLAG) -ldflags '$(MGRAGENT_LDFLAGS)' -o bin/ob_mgragent cmd/mgragent/main.go
agentctl:
$(GO) build $(BUILD_FLAG) -ldflags '$(AGENTCTL_LDFLAGS)' -o bin/ob_agentctl cmd/agentctl/main.go
agentd:
$(GO) build $(BUILD_FLAG) -ldflags '$(AGENTD_LDFLAGS)' -o bin/ob_agentd cmd/agentd/main.go
buildsucc:
@echo Build obagent successfully!
test:
runmgragent:
./bin/ob_mgragent --config tests/testdata/mgragent.yaml
runmonagent:
./bin/ob_monagent --config tests/testdata/monagent.yaml
test: pre-build pre-test
$(GOTEST) $(GOTEST_PACKAGES)
test-cover-html:
go tool cover -html=$(GOCOVERAGE_FILE)
test-cover-html-out:
mkdir -p $(GOCOVERAGE_REPORT)
go tool cover -html=$(GOCOVERAGE_FILE) -o $(GOCOVERAGE_REPORT)/index.html
test-cover-profile:
go tool cover -func=$(GOCOVERAGE_FILE)
test-cover-total:
go tool cover -func=$(GOCOVERAGE_FILE) | tail -1 | awk '{print "total line coverage: " $$3}'
deploy:
mkdir /home/admin/obagent
cp -r bin /home/admin/obagent/bin
cp -r etc /home/admin/obagent/conf
mkdir -p /home/admin/obagent/{log,run,tmp,backup,pkg_store,task_store,position_store}
fmt:
@gofmt -s -w $(filter-out , $(GOFILES))
......@@ -40,11 +100,17 @@ fmt-check:
tidy:
$(GO) mod tidy
get:
$(GO) install github.com/go-bindata/go-bindata/...@v3.1.2+incompatible
$(GO) install github.com/golang/mock/mockgen@v1.6.0
vet:
go vet $$(go list ./...)
clean:
rm -rf $(GOCOVERAGE_FILE)
rm -rf tests/mock/*
rm -rf bin/monagent
rm -rf bin/ob_mgragent bin/ob_monagent bin/ob_agentctl bin/ob_agentd
$(GO) clean -i ./...
init: pre-build pre-test tidy
PROJECT=obagent
PROCESSOR=2
VERSION=1.0
VERSION=4.1.0-SNAPSHOT
PWD ?= $(shell pwd)
GO := GO111MODULE=on GOPROXY=https://mirrors.aliyun.com/goproxy/,direct go
GO := GO111MODULE=on GOPROXY=https://goproxy.cn,direct go
BUILD_FLAG := -p $(PROCESSOR)
GOBUILD := $(GO) build $(BUILD_FLAG)
GOBUILDCOVERAGE := $(GO) test -covermode=count -coverpkg="../..." -c .
......@@ -11,14 +11,20 @@ GOCOVERAGE_FILE := tests/coverage.out
GOCOVERAGE_REPORT := tests/coverage-report
GOTEST := OB_AGENT_CONFIG_PATH=$(PWD) $(GO) test -tags test -covermode=count -coverprofile=$(GOCOVERAGE_FILE) -p $(PROCESSOR)
GO_RACE_FLAG =-race
LDFLAGS += -X "github.com/oceanbase/obagent/config.AgentVersion=${VERSION}"
#LDFLAGS += -X "github.com/oceanbase/obagent/config.ReleaseVersion=$(shell git describe --tags --dirty --always)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildTimestamp=$(shell date -u '+%Y-%m-%d %H:%M:%S')"
LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildEpoch=$(shell date '+%s')"
LDFLAGS += -X "github.com/oceanbase/obagent/config.BuildGoVersion=$(shell go version)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.GitBranch=$(shell git rev-parse --abbrev-ref HEAD)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.GitHash=$(shell git rev-parse HEAD)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.GitCommitId=$(shell git rev-parse HEAD)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.GitShortCommitId=$(shell git rev-parse --short HEAD)"
LDFLAGS += -X "github.com/oceanbase/obagent/config.GitCommitTime=$(shell git log -1 --format=%cd)"
LDFLAGS_DEBUG = -X "github.com/oceanbase/obagent/config.Mode=debug"
LDFLAGS_RELEASE = -X "github.com/oceanbase/obagent/config.Mode=release"
MONAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=monagent"
MGRAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_mgragent"
MONAGENT_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_monagent"
AGENTCTL_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_agentctl"
AGENTD_LDFLAGS = $(LDFLAGS) -X "github.com/oceanbase/obagent/config.CurProcess=ob_agentd"
GOFILES ?= $(shell git ls-files '*.go')
GOTEST_PACKAGES = $(shell go list ./... | grep -v -f tests/excludes.txt)
......
# OBAgent
OBAgent 是一个监控采集框架。OBAgent 支持推、拉两种数据采集模式,可以满足不同的应用场景。OBAgent 默认支持的插件包括主机数据采集、OceanBase 数据库指标的采集、监控数据标签处理和 Prometheus 协议的 HTTP 服务。要使 OBAgent 支持其他数据源的采集,或者自定义数据的处理流程,您只需要开发对应的插件即可。
## 许可证
OBAgent 使用 [MulanPSL - 2.0](http://license.coscl.org.cn/MulanPSL2) 许可证。您可以免费复制及使用源代码。当您修改或分发源代码时,请遵守木兰协议。
## 文档
参考 [OBAgent 文档](docs/about-obagent/what-is-obagent.md)
## 如何获取
### 环境依赖
构建 OBAgent 需要 Go 1.14 版本及以上。
### RPM 包
OBAgent 提供 RPM 包,您可以去 [Release 页面](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) 下载 RPM 包,然后使用以下命令安装:
```bash
rpm -ivh obagent-1.0.0-1.el7.x86_64.rpm
```
### 通过源码构建
#### Debug 模式
```bash
make build // make build will be debug mode by default
make build-debug
```
#### Release 模式
```bash
make build-release
```
## 如何开发
您可以为 OBAgent 开发插件。更多信息,参考 [OBAgent 插件开发](docs/develop-guide.md)
## 如何贡献
我们十分欢迎并感谢您为我们贡献。以下是您参与贡献的几种方式:
- 向我们提 [Issue](https://github.com/oceanbase/obagent/issues)
## 获取帮助
如果您在使用 OBAgent 时遇到任何问题,欢迎通过以下方式寻求帮助:
- [GitHub Issue](https://github.com/oceanbase/obagent/issues)
- [官方网站](https://open.oceanbase.com/)
- 知识问答(Coming soon)
# OBAgent
# OB-Agent
OB Agent 是 OCP Express 远程访问主机和 OBServer 的入口,提供两个主要能力:
1. 运维主机和 OBServer;
2. 收集主机、OBServer的监控。
此外,OB Agent 还承担 OB 日志查询和日志清理的职能。
OBAgent is a monitor collection framework. OBAgent supplies pull and push mode data collection to meet different applications. By default, OBAgent supports these plugins: server data collection, OceanBase Database metrics collection, monitor data processing, and the HTTP service for Prometheus Protocol. To support data collection for other data sources, or customized data flow processes, you only need to develop plugins.
## 目录结构
## Licencing
OBAgent is under [MulanPSL - 2.0](http://license.coscl.org.cn/MulanPSL2) license. You can freely copy and use the source code. When you modify or distribute the source code, please obey the MulanPSL - 2.0 license.
## Documentation
```
obagent
├── cmd:程序主入口
│ ├── mgragent: 运维进程主入口,后台运行的进程,不期望被用户使用;目标产物名称为 ob_mgragent。
│ ├── monagent: 监控进程主入口,后台运行进程,不期望被用户使用;目标产物名称为 ob_monagent。
│ ├── agentd: 守护进程,后台运行的进程,负责拉起异常退出的ob_mgragent和ob_monagent进程;目标产物名称为ob_agentd。
│ └── agentctl: 黑屏运维工具主入口,命令行运维工具,可通过该工具运维 ob_mgragent 和 ob_monagent 进程;目标产物名称为 ob_agentctl 。
├── api:请求处理、认证
│ ├── monroute:监控模块route和handlers
│ ├── mgrroute:运维模块route和handlers
│ ├── web:http server
│ └── common:公共handlers和middleware
├── agentd:守护进程agentd模块
│ └── api:进程状态信息
├── config: 配置文件解析,回调函数注册
│ ├── monconfig:监控配置
│ ├── mgrconfig:运维配置
│ ├── agentctl:黑屏运维工具配置
│ └── sdk:配置管理,回调函数注册
├── executor:提供运维能力,命令执行能力,支持 shell 跨平台执行
│ ├── agent:agent进程运维
│ ├── cleaner:日志清理
│ ├── log_query:日志查询
│ └── ...
├── lib:通用方法,比如加密、脱敏、重试、命令行执行等
│ ├── command:命令执行,异步任务调度
│ ├── process:进程启停
│ ├── goroutinepool:任务池
│ ├── log_analyzer:日志解析
│ ├── retry:重试框架
│ ├── shellf:命令模板配置解析,命令构建
│ ├── shell:命令执行
│ └── ...
├── monitor:监控模块,包含插件定义、流水线引擎、监控数据结构等
│ ├── engine:流水线引擎
│ ├── plugins:流水线插件
│ ├── message:监控数据结构
│ └── utils:监控通用函数
├── stat:自监控模块,obagent、moniotr 都会依赖此模块实现自监控
├── log:日志框架
├── errors:错误处理
├── rpm:rpm打包逻辑
├── tests:测试脚本、数据、配置
├── etc:发布的配置文件,均为 yaml 类型
│ ├── config_properties:key-value配置
│ ├── module_config:流水线等配置文件
│ ├── prometheus_config:prometheus拉取配置
│ ├── scripts:开机自启动脚本
│ └── shell_templates:命令模板
└── docs:文档,包括 obagent 的 README 文档,以及各个子模块的说明文档
```
See [OBAgent Document](docs/about-obagent/what-is-obagent.md).
# 安装 OBAgent
## How to get
您可以使用 RPM 包或者构建源码安装 OBAgent。
### Dependencies
## 环境依赖
To build OBAgent, make sure that your Go version is 1.14 or above.
构建 OBAgent 需要 Go 1.19 版本及以上。
### From RPM package
## RPM 包
OBAgent supplies RPM package. You can download it from the [Release page](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) and install it by using this command:
OBAgent 提供 RPM 包,您可以去 [Release 页面](https://mirrors.aliyun.com/oceanbase/community/stable/el/7/x86_64/) 下载 RPM 包,然后使用以下命令安装:
```bash
rpm -ivh obagent-1.0.0-1.el7.x86_64.rpm
rpm -ivh obagent-4.1.0-1.el7.x86_64.rpm
```
### From source code
## 构建源码
### Debug mode
### Debug 模式
```bash
make build // make build is debug mode by default
make build // make build will be debug mode by default
make build-debug
```
### Release mode
### Release 模式
```bash
make build-release
```
## How to develop
You can develop plugins for OBAgent. For more information, see [Develop plugins for OBAgent](docs/develop-guide.md).
## Contributing
Contributions are warmly welcomed and greatly appreciated. Here are a few ways you can contribute:
- Raise us an [Issue](https://github.com/oceanbase/obagent/issues).
## Support
In case you have any problems when using OBAgent, welcome to reach out for help:
- [GitHub Issue](https://github.com/oceanbase/obagent/issues)
- [Official website](https://open.oceanbase.com/)
- Knowledge base [Coming soon]
package api
import (
"github.com/oceanbase/obagent/lib/http"
)
type Status struct {
// agentd state
State http.State `json:"state"`
// whether agentd and all services are running
Ready bool `json:"ready"`
// agentd version
Version string `json:"version"`
// pid of agentd
Pid int `json:"pid"`
// socket file path
Socket string `json:"socket"`
// services (mgragent, monagent) status
Services map[string]ServiceStatus `json:"services"`
// services without agentd. maybe agentd dead, or service not exited expectedly
Dangling []DanglingService `json:"dangling"`
// StartAt is start time of agentd
StartAt int64 `json:"startAt"`
}
type ServiceStatus struct {
http.Status
Socket string `json:"socket"`
EndAt int64 `json:"endAt"`
}
type DanglingService struct {
Name string `json:"name"`
Pid int `json:"pid"`
PidFile string `json:"pidFile"`
Socket string `json:"socket"`
}
type StartStopAgentParam struct {
Service string `json:"service"`
}
package agentd
import (
"time"
"github.com/alecthomas/units"
)
// ServiceConfig sub service config
type ServiceConfig struct {
//Program can be absolute or relative path or just command name
Program string `yaml:"program"`
//Args program arguments
Args []string `yaml:"args"`
//Cwd current work dir of service
Cwd string `yaml:"cwd"`
//RunDir dir to put something like pid, socket, lock
RunDir string `yaml:"runDir"`
//Path to redirect Stdout
Stdout string `yaml:"stdout"`
//Path to redirect Stderr
Stderr string `yaml:"stderr"`
//When service quited too quickly for QuickExitLimit times, service will not restart even more.
QuickExitLimit int `yaml:"quickExitLimit"`
//Services lives less then MinLiveTime will treated as quited too quickly.
MinLiveTime time.Duration `yaml:"minLiveTime"`
//KillWait after stop service and process not exited, will send SIGKILL signal to it.
//0 means no wait and won't send SIGKILL signal.
KillWait time.Duration `yaml:"killWait"`
//FinalWait after stop service and process not exited, will not wait for it. and return an error
FinalWait time.Duration `yaml:"finalWait"`
//Limit cpu and memory usage
Limit LimitConfig `yaml:"limit"`
}
// Config agentd config
type Config struct {
//RunDir dir to put something like pid, socket, lock
RunDir string `yaml:"runDir"`
//LogDir dir to write agentd log
LogDir string `yaml:"logDir"`
//LogLevel highest level to log
LogLevel string `yaml:"logLevel"`
//Services sub services config
Services map[string]ServiceConfig `yaml:"services"`
//CleanupDangling whether cleanup dangling service process or not
CleanupDangling bool
}
type LimitConfig struct {
//CpuQuota max cpu usage percentage. 1.0 means 100%, 2.0 means 200%
CpuQuota float32 `yaml:"cpuQuota"`
//MemoryQuota max memory limit in bytes
MemoryQuota units.Base2Bytes `yaml:"memoryQuota"`
}
package agentd
import (
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
)
func TestLimitConfigUnmarshal(t *testing.T) {
content := `cpuQuota: 1.0
memoryQuota: 1024MB`
var conf LimitConfig
err := yaml.Unmarshal([]byte(content), &conf)
assert.Nil(t, err)
assert.Equal(t, int64(1024*1024*1024), int64(conf.MemoryQuota))
logrus.Infof("conf: %+v", conf)
}
func TestEmptyQuotaLimitConfigUnmarshal(t *testing.T) {
content := `cpuQuota:
memoryQuota: `
var conf LimitConfig
err := yaml.Unmarshal([]byte(content), &conf)
assert.Nil(t, err)
assert.Equal(t, int64(0), int64(conf.MemoryQuota))
logrus.Infof("conf: %+v", conf)
}
package agentd
import "github.com/oceanbase/obagent/lib/errors"
const module = "agentd"
var (
ServiceAlreadyStartedErr = errors.FailedPrecondition.NewCode(module, "service_already_started")
ServiceAlreadyStoppedErr = errors.FailedPrecondition.NewCode(module, "service_already_stopped")
ServiceNotFoundErr = errors.NotFound.NewCode(module, "service_not_round")
BadParamErr = errors.InvalidArgument.NewCode(module, "bad_param").WithMessageTemplate("invalid input parameter, maybe bad format: %v")
InvalidActionErr = errors.InvalidArgument.NewCode(module, "invalid_action").WithMessageTemplate("invalid action: %s")
InternalServiceErr = errors.Internal.NewCode(module, "internal_service_err")
AgentdNotRunningErr = errors.Internal.NewCode(module, "agentd_not_running")
WritePidFailedErr = errors.Internal.NewCode(module, "write_pid_failed")
RemovePidFailedErr = errors.Internal.NewCode(module, "remove_pid_failed")
)
package agentd
type Limiter interface {
LimitPid(pid int) error
}
func NewLimiter(name string, conf LimitConfig) (Limiter, error) {
return newLimiter(name, conf)
}
type NopLimiter struct {
}
func (l *NopLimiter) LimitPid(pid int) error {
return nil
}
//go:build linux
// +build linux
package agentd
import (
"path/filepath"
"github.com/containerd/cgroups"
"github.com/opencontainers/runtime-spec/specs-go"
log "github.com/sirupsen/logrus"
)
type LinuxLimiter struct {
cgroup cgroups.Cgroup
}
func newLimiter(name string, conf LimitConfig) (Limiter, error) {
if conf.CpuQuota <= 0 && conf.MemoryQuota <= 0 {
log.Infof("create service %s resource limit skipped, no limit in config", name)
return &LinuxLimiter{}, nil
}
cg, err := cgroups.New(cgroups.V1, cgroups.StaticPath(filepath.Join("/ocp_agent/", name)), toLinuxResources(conf))
if err != nil {
log.Warnf("create cgroup for service %s failed, fallback to watch limiter. only memory quota will affect!", name)
return &WatchLimiter{
name: name,
conf: conf,
}, nil
}
log.Infof("create service %s resource limit done, cpu: %v, memory: %v", name, conf.CpuQuota, conf.MemoryQuota)
return &LinuxLimiter{
cgroup: cg,
}, nil
}
func (l *LinuxLimiter) LimitPid(pid int) error {
if l.cgroup == nil {
return nil
}
err := l.cgroup.Add(cgroups.Process{Pid: pid})
return err
}
func toLinuxResources(conf LimitConfig) *specs.LinuxResources {
var period *uint64 = nil
var quota *int64 = nil
var memLimit *int64 = nil
if conf.CpuQuota > 0 {
period = new(uint64)
*period = 1000000
quota = new(int64)
*quota = int64(1000000 * conf.CpuQuota)
}
if conf.MemoryQuota > 0 {
memLimit = new(int64)
*memLimit = int64(conf.MemoryQuota)
}
return &specs.LinuxResources{
CPU: &specs.LinuxCPU{
Period: period,
Quota: quota,
},
Memory: &specs.LinuxMemory{
Limit: memLimit,
},
}
}
//go:build !linux
// +build !linux
package agentd
import (
log "github.com/sirupsen/logrus"
)
func newLimiter(name string, conf LimitConfig) (Limiter, error) {
log.Infof("creating service %s resource limit done, WatchLimiter memory: %v", name, conf.MemoryQuota)
return &WatchLimiter{name: name, conf: conf}, nil
}
package agentd
import (
"time"
"github.com/shirou/gopsutil/v3/process"
log "github.com/sirupsen/logrus"
)
type WatchLimiter struct {
name string
conf LimitConfig
}
func (l *WatchLimiter) LimitPid(pid int) error {
if l.conf.MemoryQuota <= 0 {
return nil
}
p, err := process.NewProcess(int32(pid))
if err != nil {
return err
}
go func() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
<-ticker.C
m, mErr := p.MemoryInfo()
if mErr != nil {
log.Warnf("fetch memory info for service %s failed: %v, stop watch loop", l.name, mErr)
return // maybe process exited
}
if m.RSS > uint64(l.conf.MemoryQuota) {
log.Warnf("service %s exceed the memory quota: %d, kill process %d", l.name, l.conf.MemoryQuota, p.Pid)
_ = p.Kill()
}
}
}()
return nil
}
package agentd
import (
"fmt"
"os"
"syscall"
"time"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/agentd/api"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/lib/path"
"github.com/oceanbase/obagent/lib/process"
)
// serviceProc creates a proc via ServiceConfig
func serviceProc(conf ServiceConfig) *process.Proc {
return process.NewProc(toProcConfig(conf))
}
func toProcConfig(conf ServiceConfig) process.ProcessConfig {
if conf.Cwd == "" {
conf.Cwd = path.AgentDir()
}
return process.ProcessConfig{
Program: conf.Program,
Args: conf.Args,
Stdout: conf.Stdout,
Stderr: conf.Stderr,
Cwd: conf.Cwd,
InheritEnv: true,
KillWait: conf.KillWait,
FinalWait: conf.FinalWait,
}
}
type Service struct {
name string
conf ServiceConfig
proc *process.Proc
done chan struct{}
limiter Limiter
state *http.StateHolder
}
type TaskParam struct {
Action string `json:"action"`
}
func NewService(name string, conf ServiceConfig) *Service {
proc := serviceProc(conf)
limiter, err := NewLimiter(name, conf.Limit)
if err != nil {
log.WithError(err).Errorf("failed to create limiter for service '%s' with config %+v", name, conf.Limit)
limiter = &NopLimiter{}
}
ret := &Service{
name: name,
conf: conf,
proc: proc,
done: nil,
limiter: limiter,
state: http.NewStateHolder(http.Stopped),
}
return ret
}
func timeToMill(t time.Time) int64 {
ret := t.UnixNano() / time.Millisecond.Nanoseconds()
if ret > 0 {
return ret
}
return 0
}
type ServiceState struct {
//Running whether the process is running
Running bool `json:"running"`
//Success whether the process is exited with code 0
Success bool `json:"success"`
//Exited whether the process is exited
Exited bool `json:"exited"`
//Pid of the process
Pid int `json:"Pid"`
//ExitCode of the finished process
ExitCode int `json:"exit_code"`
//StarAt time of the process started
StartAt int64 `json:"start_at"`
//EndAt time of the process ended
EndAt int64 `json:"end_at"`
Status string `json:"status"`
}
func (s *Service) Start() (err error) {
s.cleanup()
if s.state.Get() == http.Running {
return nil
}
if !s.state.Cas(http.Stopped, http.Starting) {
err = ServiceAlreadyStartedErr.NewError(s.name)
log.WithField("service", s.name).WithError(err).Warn("service already started")
return err
}
s.done = make(chan struct{})
err = s.startProc()
if err != nil {
s.state.Set(http.Stopped)
return
}
go s.guard()
return nil
}
func (s *Service) startProc() (err error) {
s.cleanup()
pidFile, err := createPid(s.pidPath())
if err != nil {
return err
}
defer pidFile.Close()
params := map[string]string{
"run_dir": s.conf.RunDir,
"conf_dir": path.ConfDir(),
"bin_dir": path.BinDir(),
"agent_dir": path.AgentDir(),
}
log.WithField("service", s.name).Infof("starting service")
err = s.proc.Start(params)
if err != nil {
err = InternalServiceErr.NewError(s.name).WithCause(err)
s.cleanup()
return err
}
log.WithField("service", s.name).Infof("service process started. pid: %d", s.Pid())
err = writePid(pidFile, s.Pid())
if err != nil {
log.WithField("service", s.name).WithError(err).Errorf("write pid file failed %s", s.pidPath())
}
return
}
func (s *Service) waitExit() {
select {
case <-s.proc.Done():
case <-time.After(time.Second):
}
}
func (s *Service) limitResource() {
err := s.limiter.LimitPid(s.Pid())
if err != nil {
log.WithField("service", s.name).WithError(err).Errorf("limit service resource failed. pid=%d", s.Pid())
}
}
func (s *Service) guard() {
var err error
quickExitCount := 0
defer func() {
s.state.Set(http.Stopped)
s.cleanup()
close(s.done)
}()
s.limitResource()
for {
//todo wait for s.ready()
s.state.Cas(http.Starting, http.Running)
//s.queryStatus()
s.waitExit()
svcState := s.state.Get()
state := s.proc.State()
if state.Exited {
log.WithField("service", s.name).Warnf("service exited with code %d. service state: %v", state.ExitCode, svcState)
}
// service is stopped by watchdog, exit guard
if svcState == http.Stopping || svcState == http.Stopped {
log.WithField("service", s.name).Infof("service stopped. service state: %s", svcState)
return
}
if !state.Exited {
// still running...
continue
}
// process exited
if state.ExitCode == 0 {
log.WithField("service", s.name).Info("service normally exited")
return
}
s.state.Set(http.Stopped)
liveTime := state.EndAt.Sub(state.StartAt)
if s.conf.MinLiveTime > 0 && liveTime < s.conf.MinLiveTime {
quickExitCount++
log.WithField("service", s.name).Warnf("service exited too quickly. live time: %d, MinLiveTime: %d, count: %d", liveTime, s.conf.MinLiveTime, quickExitCount)
if quickExitCount >= s.conf.QuickExitLimit {
log.WithField("service", s.name).Errorf("service exited too quickly. live time: %d, MinLiveTime: %d, count: %d", liveTime, s.conf.MinLiveTime, quickExitCount)
return
}
} else {
quickExitCount = 0
}
log.WithField("service", s.name).Info("recovering service")
s.state.Set(http.Starting)
err = s.startProc()
if err != nil {
s.state.Set(http.Stopped)
log.WithField("service", s.name).WithError(err).Error("start service got error")
return
}
s.limitResource()
}
}
func (s *Service) Stop() (err error) {
if s.state.Get() == http.Stopped {
return nil
}
s.state.Set(http.Stopping) // state may in running, staring, stopping
log.WithField("service", s.name).Info("stopping service")
err = s.proc.Stop()
if err != nil {
err = InternalServiceErr.NewError(s.name).WithCause(err)
log.WithField("service", s.name).WithField("pid", s.Pid()).WithError(err).Warn("stop service got error")
state := s.State()
if state.State == http.Stopping || state.State == http.Stopped {
s.state.Set(state.State)
return nil
} else {
log.WithField("service", s.name).WithField("pid", s.Pid()).Warn("service did not handle TERM signal properly, try KILL it")
err = s.proc.Kill()
}
}
return
}
func (s *Service) Pid() int {
if s.proc == nil {
return 0
}
return s.proc.Pid()
}
func (s *Service) cleanup() {
socketPath := s.socketPath()
if isSocket(socketPath) {
//log.WithField("service", s.name).Info("removing socket file %s", socketPath)
_ = os.Remove(socketPath)
}
_ = removePid(s.pidPath(), s.Pid())
_ = removePid(s.backupPidPath(), s.Pid())
}
func (s *Service) socketPath() string {
return agent.SocketPath(s.conf.RunDir, s.name, s.Pid())
}
func (s *Service) pidPath() string {
return agent.PidPath(s.conf.RunDir, s.name)
}
func (s *Service) backupPidPath() string {
return agent.BackupPidPath(s.conf.RunDir, s.name, s.Pid())
}
func (s *Service) State() api.ServiceStatus {
state := s.proc.State()
svcState := http.Unknown
if state.Running {
ret, err := s.queryStatus()
if err == nil {
return api.ServiceStatus{
Status: ret,
Socket: agent.SocketPath(s.conf.RunDir, s.name, s.Pid()),
EndAt: state.EndAt.UnixNano(),
}
}
} else {
svcState = http.Stopped
}
return api.ServiceStatus{
Status: http.Status{
State: svcState,
Pid: state.Pid,
StartAt: state.StartAt.UnixNano(),
},
Socket: agent.SocketPath(s.conf.RunDir, s.name, s.Pid()),
EndAt: state.EndAt.UnixNano(),
}
}
func (s *Service) queryStatus() (http.Status, error) {
readyResult := http.Status{}
c := s.apiClient()
if c == nil {
return readyResult, http.NoApiClientErr.NewError()
}
err := c.Call("/api/v1/status", nil, &readyResult)
if err != nil {
return readyResult, err
}
return readyResult, nil
}
func (s *Service) apiClient() *http.ApiClient {
socketPath := s.socketPath()
if isSocket(socketPath) {
return http.NewSocketApiClient(socketPath, time.Second*5)
}
return nil
}
func createPid(pidPath string) (*os.File, error) {
ret, err := os.OpenFile(pidPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|syscall.O_CLOEXEC, 0644)
if err != nil {
return nil, WritePidFailedErr.NewError(pidPath).WithCause(err)
}
return ret, err
}
func writePid(f *os.File, pid int) error {
_, err := fmt.Fprintf(f, "%d\n", pid)
if err != nil {
return WritePidFailedErr.NewError(f.Name()).WithCause(err)
}
return nil
}
func removePid(pidPath string, expectedPid int) error {
pid, err := agent.ReadPid(pidPath)
if err != nil {
return RemovePidFailedErr.NewError(pidPath).WithCause(err)
}
if pid != expectedPid {
return nil
}
log.Infof("remove pid file %s", pidPath)
err = os.Remove(pidPath)
if err != nil {
return RemovePidFailedErr.NewError(pidPath).WithCause(err)
}
return nil
}
func isSocket(path string) bool {
stat, err := os.Stat(path)
return err == nil && (stat.Mode()&os.ModeSocket != 0)
}
package agentd
import (
"testing"
"github.com/oceanbase/obagent/lib/path"
)
func Test_serviceProc(t *testing.T) {
conf := toProcConfig(ServiceConfig{})
if conf.Cwd != path.AgentDir() {
t.Error("default cwd wrong")
}
if !conf.InheritEnv {
t.Error("InheritEnv should be true")
}
}
package agentd
import (
"fmt"
"net/http"
"net/http/pprof"
"os"
"os/signal"
"path"
"path/filepath"
"syscall"
"time"
"github.com/shirou/gopsutil/v3/process"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/unix"
"github.com/oceanbase/obagent/agentd/api"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/command"
http2 "github.com/oceanbase/obagent/lib/http"
path2 "github.com/oceanbase/obagent/lib/path"
)
// Agentd supervisor process for agents
// start, stop sub services, view status of self and sub services
type Agentd struct {
config Config
services map[string]*Service
listener *http2.Listener
state *http2.StateHolder
}
// NewAgentd create a new Agentd via config
func NewAgentd(config Config) *Agentd {
listener := http2.NewListener()
services := make(map[string]*Service)
for name, svcConf := range config.Services {
svc := NewService(name, svcConf)
services[name] = svc
}
ret := &Agentd{
config: config,
services: services,
listener: listener,
state: http2.NewStateHolder(http2.Stopped),
}
ret.initRoutes()
return ret
}
var startAt = time.Now().UnixNano()
func (w *Agentd) initRoutes() {
rootPath := "/api/v1"
statusHandler := http2.NewHandler(command.WrapFunc(w.Status))
startServiceHandler := http2.NewHandler(command.WrapFunc(func(param api.StartStopAgentParam) error {
return w.StartService(param.Service)
}))
stopServiceHandler := http2.NewHandler(command.WrapFunc(func(param api.StartStopAgentParam) error {
return w.StopService(param.Service)
}))
w.listener.AddHandler(path.Join(rootPath, "/status"), statusHandler)
w.listener.AddHandler(path.Join(rootPath, "/startService"), startServiceHandler)
w.listener.AddHandler(path.Join(rootPath, "/stopService"), stopServiceHandler)
w.listener.AddHandler("/debug/pprof/", http.HandlerFunc(pprof.Index))
w.listener.AddHandler("/debug/pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
w.listener.AddHandler("/debug/pprof/profile", http.HandlerFunc(pprof.Profile))
w.listener.AddHandler("/debug/pprof/symbol", http.HandlerFunc(pprof.Symbol))
w.listener.AddHandler("/debug/pprof/trace", http.HandlerFunc(pprof.Trace))
}
// Start agentd and sub services
func (w *Agentd) Start() error {
w.cleanup()
if w.state.Get() == http2.Running {
return nil
}
log.Info("starting agentd")
var err error
err = w.writePid()
if err != nil {
return err
}
w.state.Set(http2.Starting)
err = os.MkdirAll(w.config.RunDir, 0755)
if err != nil {
return err
}
socketPath := w.socketPath()
log.Infof("starting socket listener on '%s'", socketPath)
err = w.listener.StartSocket(socketPath)
if err != nil {
log.Errorf("start socket listener '%s' failed: %v", socketPath, err)
_ = w.Stop()
return err
}
w.state.Set(http2.Running)
for name, svc := range w.services {
log.Infof("starting service '%s'", name)
err = svc.Start()
if err != nil {
log.Errorf("start service '%s' failed: %s", name, err)
}
}
log.Info("agentd started")
return nil
}
// Stop agentd and sub services
func (w *Agentd) Stop() error {
if w.state.Get() == http2.Stopped {
return nil
}
log.Info("stopping agentd")
if !w.state.Cas(http2.Running, http2.Stopping) {
return AgentdNotRunningErr.NewError(w.state.Get())
}
for name, svc := range w.services {
state := svc.State().State
log.Infof("stopping service '%s'. current state: %s", name, state)
if state == http2.Stopped {
log.Infof("service '%s' already stopped. State: %s", name, state)
continue
}
err := svc.Stop()
if err != nil {
log.Errorf("stop service '%s' got error: %v", name, err)
}
}
w.state.Set(http2.Stopped)
log.Info("agentd stopped")
w.listener.Close()
err := w.removePid()
if err != nil {
log.Warn("remove pid file failed: ", err)
}
w.cleanup()
return nil
}
// ListenSignal capture SIGTERM and SIGINT, do a normal Stop
func (w *Agentd) ListenSignal() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
select {
case sig := <-ch:
log.Infof("signal '%s' received. exiting...", sig.String())
_ = w.Stop()
}
}
// Status returns agentd and sub services status
func (w *Agentd) Status() api.Status {
svcStates := make(map[string]api.ServiceStatus)
ready := w.state.Get() == http2.Running
for name, svc := range w.services {
state := svc.State()
svcStates[name] = state
if state.State != http2.Running {
ready = false
}
}
dangling := w.danglingServices()
socket := w.socketPath()
if !isSocket(socket) {
socket = ""
}
return api.Status{
State: w.state.Get(),
Ready: ready,
Pid: os.Getpid(),
Socket: socket,
Services: svcStates,
Dangling: dangling,
Version: config.AgentVersion,
StartAt: startAt,
}
}
func (w *Agentd) StartService(name string) error {
service, ok := w.services[name]
if !ok {
return ServiceNotFoundErr.NewError(name)
}
return service.Start()
}
func (w *Agentd) StopService(name string) error {
service, ok := w.services[name]
if !ok {
return ServiceNotFoundErr.NewError(name)
}
return service.Stop()
}
func (w *Agentd) socketPath() string {
return agent.SocketPath(w.config.RunDir, path2.Agentd, os.Getpid())
}
func (w *Agentd) pidPath() string {
return agent.PidPath(w.config.RunDir, path2.Agentd)
}
func (w *Agentd) writePid() error {
f, err := os.OpenFile(w.pidPath(), os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC|syscall.O_CLOEXEC, 0644)
if err != nil {
return err
}
defer f.Close()
_, err = fmt.Fprintf(f, "%d", os.Getpid())
if err != nil {
return err
}
return nil
}
func (w *Agentd) removePid() error {
return removePid(w.pidPath(), os.Getpid())
}
func (w *Agentd) cleanupPid(program, pidPath string) error {
if _, err := os.Stat(pidPath); err != nil && os.IsNotExist(err) {
return nil
}
pid, err := agent.ReadPid(pidPath)
if err != nil {
return err
}
_, err = process.NewProcess(int32(pid))
if err != nil {
return removePid(pidPath, pid)
}
return nil
}
func (w *Agentd) cleanupPidPattern(program, pattern string) {
pidPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, pattern))
if err != nil {
return
}
for _, pidPath := range pidPaths {
err = w.cleanupPid(program, pidPath)
if err != nil {
log.WithError(err).Errorf("cleanup pid file %s got error", pidPath)
}
}
}
func (w *Agentd) cleanupSocketPattern(pattern string) {
sockPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, pattern))
if err != nil {
return
}
for _, sockPath := range sockPaths {
err = w.cleanupSocket(sockPath)
if err != nil {
log.WithError(err).Errorf("cleanup socket file %s got error", sockPath)
}
}
}
func (w *Agentd) cleanupSocket(sockPath string) error {
if !isSocket(sockPath) {
return nil
}
if http2.CanConnect("unix", sockPath, time.Second) {
return nil
}
return os.Remove(sockPath)
}
func (w *Agentd) cleanupDangling() {
if !w.config.CleanupDangling {
return
}
n := len(w.danglingServices())
if n == 0 {
return
}
log.Infof("cleaning up dangling services, %d to cleanup", n)
for _, dangling := range w.danglingServices() {
log.Infof("cleaning up dangling service: %s %d", dangling.Name, dangling.Pid)
proc, err := process.NewProcess(int32(dangling.Pid))
if err != nil {
log.Warnf("get process of dangling service: %s %d got error %v", dangling.Name, dangling.Pid, err)
continue
}
err = proc.SendSignal(unix.SIGTERM)
if err != nil {
log.Warnf("terminate process of dangling service: %s %d got error %v", dangling.Name, dangling.Pid, err)
continue
}
}
}
func (w *Agentd) cleanup() {
var err error
w.cleanupDangling()
w.cleanupPidPattern(path2.Agentd, path2.Agentd+".pid")
w.cleanupPidPattern(path2.Agentd, path2.Agentd+".*.pid")
w.cleanupSocketPattern(path2.Agentd + ".*.sock")
pidPath := filepath.Join(w.config.RunDir, path2.Agentd+".pid")
err = w.cleanupPid(path2.Agentd, pidPath)
if err != nil {
log.WithError(err).Errorf("cleanup pid file %s got error", pidPath)
}
if err != nil {
log.WithError(err).Errorf("cleanup pid file %s got error", pidPath)
}
for name, conf := range w.config.Services {
w.cleanupPidPattern(conf.Program, fmt.Sprintf("%s.pid", name))
w.cleanupPidPattern(conf.Program, fmt.Sprintf("%s.*.pid", name))
w.cleanupSocketPattern(fmt.Sprintf("%s.*.sock", name))
}
}
func (w *Agentd) danglingServices() []api.DanglingService {
var ret []api.DanglingService
for name := range w.config.Services {
pidPath := agent.PidPath(w.config.RunDir, name)
pid, err := agent.ReadPid(pidPath)
if err == nil {
if w.isDangling(name, pid) {
socket := agent.SocketPath(w.config.RunDir, name, pid)
if !isSocket(socket) {
socket = ""
}
ret = append(ret, api.DanglingService{
Name: name,
Pid: pid,
PidFile: pidPath,
Socket: socket,
})
}
}
pidPaths, err := filepath.Glob(filepath.Join(w.config.RunDir, fmt.Sprintf("%s.*.pid", name)))
if err == nil {
for _, pidPath = range pidPaths {
pid, err = agent.ReadPid(pidPath)
if err != nil {
continue
}
if w.isDangling(name, pid) {
socket := agent.SocketPath(w.config.RunDir, name, pid)
if !isSocket(socket) {
socket = ""
}
ret = append(ret, api.DanglingService{
Name: name,
Pid: pid,
PidFile: pidPath,
Socket: socket,
})
}
}
}
}
return ret
}
func (w *Agentd) isDangling(program string, pid int) bool {
proc, err := process.NewProcess(int32(pid))
if err != nil {
return false
}
name, err := proc.Name()
if err != nil {
return false
}
if name != program {
return false
}
ppid, err := proc.Ppid()
if err != nil {
return false
}
return ppid == 1 || ppid == 0
}
package agentd
import (
"fmt"
"os"
"syscall"
"testing"
"time"
process2 "github.com/shirou/gopsutil/v3/process"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/tests/testutil"
)
var mockAgentPath string
func TestMain(m *testing.M) {
testutil.MakeDirs()
//err := testutil.BuildBins()
//if err != nil {
// os.Exit(2)
//}
err := testutil.BuildMockAgent()
if err != nil {
os.Exit(2)
}
ret := m.Run()
testutil.KillAll()
testutil.DelTestFiles()
os.Exit(ret)
}
func TestNewWatchdog(t *testing.T) {
defer testutil.KillAll()
testutil.MakeDirs()
defer testutil.DelTestFiles()
watchdog := NewAgentd(Config{
RunDir: testutil.RunDir,
LogDir: testutil.LogDir,
LogLevel: "info",
CleanupDangling: true,
Services: map[string]ServiceConfig{
"test": {
Program: testutil.MockAgentPath,
Args: []string{"test", testutil.RunDir, "1", "1"},
RunDir: testutil.RunDir,
FinalWait: time.Second * 2,
MinLiveTime: time.Second * 5,
QuickExitLimit: 3,
Stdout: testutil.LogDir + "/test.output.log",
Stderr: testutil.LogDir + "/test.error.log",
},
},
})
err := watchdog.Start()
if err != nil {
t.Error(err)
}
err = watchdog.Start()
if err != nil {
t.Error("duplicate start should not fail")
}
status := watchdog.Status()
if status.State != http.Running {
t.Error("watchdog state should be running")
}
time.Sleep(1500 * time.Millisecond)
status = watchdog.Status()
fmt.Printf("status: %+v\n", status)
if status.Services["test"].State != http.Running {
t.Errorf("service state should be running got '%s'", status.Services["test"].State)
}
err = watchdog.Stop()
if err != nil {
t.Error(err)
}
time.Sleep(time.Second)
for name, service := range watchdog.services {
_, err2 := process2.NewProcess(int32(service.Pid()))
if err2 == nil {
t.Errorf("service '%s' should be stopped", name)
}
}
err = watchdog.Stop()
if err != nil {
t.Error("duplicate stop should not fail")
}
}
func TestWatchdog_ListenSignal(t *testing.T) {
defer testutil.KillAll()
testutil.MakeDirs()
defer testutil.DelTestFiles()
watchdog := NewAgentd(Config{
RunDir: testutil.RunDir,
LogDir: testutil.LogDir,
LogLevel: "info",
Services: map[string]ServiceConfig{
"test": {
Program: testutil.MockAgentPath,
Args: []string{"test", testutil.RunDir, "1", "1"},
RunDir: testutil.RunDir,
FinalWait: time.Second * 2,
MinLiveTime: time.Second * 5,
QuickExitLimit: 3,
Stdout: testutil.LogDir + "/test.output.log",
Stderr: testutil.LogDir + "/test.error.log",
},
},
})
err := watchdog.Start()
if err != nil {
t.Error(err)
}
p, err := process2.NewProcess(int32(watchdog.Status().Pid))
if err != nil {
t.Error(err)
return
}
ch := make(chan struct{})
go func() {
watchdog.ListenSignal()
close(ch)
}()
time.Sleep(100 * time.Millisecond)
_ = p.SendSignal(syscall.SIGTERM)
select {
case <-ch:
// ok
case <-time.After(time.Second * 5):
t.Error("wait stop by signal timeout")
}
status := watchdog.Status()
if status.State != http.Stopped {
t.Error("watchdog should be stopped")
}
}
func TestStartFail(t *testing.T) {
defer testutil.KillAll()
testutil.MakeDirs()
defer testutil.DelTestFiles()
watchdog := NewAgentd(Config{
RunDir: testutil.RunDir,
LogDir: testutil.LogDir,
LogLevel: "info",
Services: map[string]ServiceConfig{
"test": {
Program: testutil.MockAgentPath,
Args: []string{"test", testutil.RunDir, "-1", "1"},
RunDir: testutil.RunDir,
FinalWait: time.Second * 2,
MinLiveTime: time.Second * 5,
QuickExitLimit: 0,
Stdout: testutil.LogDir + "/test.output.log",
Stderr: testutil.LogDir + "/test.error.log",
},
},
})
err := watchdog.Start()
if err != nil {
t.Error(err)
}
time.Sleep(2000 * time.Millisecond)
if watchdog.services["test"].State().State != http.Stopped {
t.Error("failed service should be 'stopped'")
}
}
package common
import (
"fmt"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/mgragent"
"github.com/oceanbase/obagent/executor/agent"
)
// http handler: validate module config and save module config
func UpdateConfigPropertiesHandler(c *gin.Context) {
kvs := config.KeyValues{}
c.Bind(&kvs)
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
configVersion, err := config.UpdateConfig(ctx, &kvs)
if err != nil {
ctxlog.Errorf("update config err:%s", err)
}
SendResponse(c, &agent.AgentctlResponse{
Successful: true,
Message: configVersion,
Error: "",
}, err)
}
// http handler: notify module config
func NotifyConfigPropertiesHandler(c *gin.Context) {
nconfig := new(config.NotifyModuleConfig)
c.Bind(nconfig)
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx).WithFields(log.Fields{
"process": nconfig.Process,
"module": nconfig.Module,
"updated key values": nconfig.UpdatedKeyValues,
})
ctxlog.Infof("notify module config")
err := config.NotifyModuleConfigForHttp(ctx, nconfig)
if err != nil {
ctxlog.Errorf("notify module config err:%+v", err)
}
SendResponse(c, &agent.AgentctlResponse{
Successful: true,
Message: "notify module config success",
Error: "",
}, err)
}
func ValidateConfigPropertiesHandler(c *gin.Context) {
kvs := config.KeyValues{}
c.Bind(&kvs)
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
ctxlog.Debugf("validate module config")
err := config.ValidateConfigKeyValues(ctx, kvs.Configs)
if err != nil {
ctxlog.Errorf("validate configs failed, err:%+v", err)
}
SendResponse(c, &agent.AgentctlResponse{
Successful: true,
Message: "success",
Error: "",
}, err)
}
// http handler: effect module config
func ConfigStatusHandler(c *gin.Context) {
needRestartModuleKeyValues := config.NeedRestartModuleKeyValues()
SendResponse(c, needRestartModuleKeyValues, nil)
}
func ReloadConfigHandler(c *gin.Context) {
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
err := mgragent.GlobalConfigManager.ReloadModuleConfigs(ctx)
if err != nil {
ctxlog.Errorf("reload module config files failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
err = config.NotifyAllModules(ctx)
if err != nil {
ctxlog.Errorf("notify config change after changing module config failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
SendResponse(c, "reload config success", nil)
}
func ChangeConfigHandler(c *gin.Context) {
req := mgragent.ModuleConfigChangeRequest{
Reload: true,
}
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
err := c.BindJSON(&req)
if err != nil {
ctxlog.Errorf("parse request failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
ctxlog.Debugf("change module config %+v", req)
changedFileNames, err := mgragent.GlobalConfigManager.ChangeModuleConfigs(ctx, &req)
if err != nil {
ctxlog.Errorf("change module config failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
ctxlog.Infof("module config files changed: %+v", changedFileNames)
if len(changedFileNames) > 0 && req.Reload {
err = mgragent.GlobalConfigManager.ReloadModuleConfigs(ctx)
if err != nil {
ctxlog.Errorf("reload module config files failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
err = config.NotifyAllModules(ctx)
if err != nil {
ctxlog.Errorf("notify config change after changing module config failed, err: %+v", err)
SendResponse(c, nil, err)
return
}
}
SendResponse(c, fmt.Sprintf("%d files changed", len(req.ModuleConfigChanges)), nil)
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package route
package common
import (
"context"
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/response"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/log"
)
// keys stored in gin.Context
const (
AgentResponseKey = "agentResponse"
TraceIdKey = "traceId"
OcpAgentResponseKey = "ocpAgentResponse"
TraceIdKey = "traceId"
OcpServerIpKey = "ocpServerIp"
)
func NewContextWithTraceId(c *gin.Context) context.Context {
......@@ -37,7 +26,7 @@ func NewContextWithTraceId(c *gin.Context) context.Context {
return context.WithValue(context.Background(), log.TraceIdKey{}, traceId)
}
func sendResponse(c *gin.Context, data interface{}, err error) {
resp := response.BuildResponse(data, err)
c.Set(AgentResponseKey, resp)
func SendResponse(c *gin.Context, data interface{}, err error) {
resp := http.BuildResponse(data, err)
c.Set(OcpAgentResponseKey, resp)
}
package common
import (
"os"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/lib/system"
)
func TimeHandler(c *gin.Context) {
SendResponse(c, time.Now(), nil)
}
func InfoHandler(c *gin.Context) {
SendResponse(c, config.GetAgentInfo(), nil)
}
func GitInfoHandler(c *gin.Context) {
SendResponse(c, config.GetGitInfo(), nil)
}
var StartAt = time.Now().UnixNano()
var libProcess system.Process = system.ProcessImpl{}
func StatusHandler(s *http.StateHolder) gin.HandlerFunc {
return func(c *gin.Context) {
ports := make([]int, 0)
pid := os.Getpid()
processInfo, err := libProcess.GetProcessInfoByPid(int32(pid))
if err != nil {
log.Errorf("StatusHandler get processInfo failed, pid:%s", pid)
} else {
ports = processInfo.Ports
}
var info = http.Status{
State: s.Get(),
Version: config.AgentVersion,
Pid: pid,
StartAt: StartAt,
Ports: ports,
}
SendResponse(c, info, nil)
}
}
package common
import (
"bytes"
"fmt"
"io/ioutil"
"regexp"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/go-playground/validator/v10"
prom "github.com/prometheus/client_golang/prometheus"
"github.com/sirupsen/logrus"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/errors"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/lib/system"
"github.com/oceanbase/obagent/lib/trace"
"github.com/oceanbase/obagent/stat"
)
var libSystem system.System = system.SystemImpl{}
const statusURI = "/api/v1/status"
const logQuerierURI = "/api/v1/log"
// Before handlers, extract HTTP headers, and log the API request.
func PreHandlers(maskBodyRoutes ...string) func(*gin.Context) {
return func(c *gin.Context) {
if c.Request.RequestURI == statusURI {
c.Next()
return
}
// Use traceId passed from OCP-Server for logging.
traceId := trace.GetTraceId(c.Request)
c.Set(TraceIdKey, traceId)
// Store OCP-Server's ip address for logging.
// c.ClientIP() may not be accurate if HTTP requests are forwarded by proxy server.
ocpServerIp := c.Request.Header.Get(trace.OcpServerIpHeader)
c.Set(OcpServerIpKey, ocpServerIp)
ctx := NewContextWithTraceId(c)
masked := false
for _, it := range maskBodyRoutes {
if strings.HasPrefix(c.Request.RequestURI, it) {
masked = true
}
}
if masked {
log.WithContext(ctx).Infof("API request: [%v %v, client=%v, ocpServerIp=%v, traceId=%v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, traceId)
} else {
body := readRequestBody(c)
log.WithContext(ctx).Infof("API request: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, body=%v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, traceId, body)
}
c.Next()
}
}
var emptyRe = regexp.MustCompile(`\s+`)
func readRequestBody(c *gin.Context) string {
body, _ := ioutil.ReadAll(c.Request.Body)
c.Request.Body = ioutil.NopCloser(bytes.NewReader(body))
return emptyRe.ReplaceAllString(string(body), "")
}
// If c.BindJSON fails (e.g. validation error), content-type will be set to text/plain.
// So set content-type before handlers.
func SetContentType(c *gin.Context) {
c.Writer.Header().Set("Content-Type", "application/json")
c.Next()
}
func getResponseFromContext(c *gin.Context) http.OcpAgentResponse {
ctx := NewContextWithTraceId(c)
if len(c.Errors) > 0 {
var subErrors []interface{}
for _, e := range c.Errors {
switch e.Type {
case gin.ErrorTypeBind:
validationErrors := e.Err.(validator.ValidationErrors)
for _, fieldError := range validationErrors {
subErrors = append(subErrors, http.NewApiFieldError(fieldError))
}
default:
subErrors = append(subErrors, http.ApiUnknownError{Error: e.Err})
}
}
return http.NewSubErrorsResponse(subErrors)
}
if r, ok := c.Get(OcpAgentResponseKey); ok {
if resp, ok := r.(http.OcpAgentResponse); ok {
return resp
}
}
log.WithContext(ctx).Error("no response object found from gin context")
return http.NewErrorResponse(errors.Occur(errors.ErrUnexpected, "cannot build response body"))
}
// After handlers, build the complete OcpAgentResponse object,
// log the API result, and send HTTP response.
func PostHandlers(excludeRoutes ...string) func(*gin.Context) {
localIpAddress, _ := libSystem.GetLocalIpAddress()
return func(c *gin.Context) {
for _, it := range excludeRoutes {
if strings.HasPrefix(c.Request.RequestURI, it) {
c.Next()
return
}
}
startTime := time.Now()
c.Next()
ctx := NewContextWithTraceId(c)
resp := getResponseFromContext(c)
duration := time.Now().Sub(startTime)
resp.Duration = int(duration / time.Millisecond)
ocpServerIp, _ := c.Get(OcpServerIpKey)
if v, ok := c.Get(TraceIdKey); ok {
if traceId, ok := v.(string); ok {
resp.TraceId = traceId
}
}
resp.Server = localIpAddress
if resp.Successful {
if c.Request.RequestURI != statusURI {
if strings.HasPrefix(c.Request.RequestURI, logQuerierURI) {
log.WithContext(ctx).Infof("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status)
} else {
log.WithContext(ctx).Infof("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, data=%+v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Data)
}
} else {
log.WithContext(ctx).Debugf("API response OK: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, data=%+v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Data)
}
} else {
log.WithContext(ctx).Infof("API response error: [%v %v, client=%v, ocpServerIp=%v, traceId=%v, duration=%v, status=%v, error=%v]",
c.Request.Method, c.Request.URL, c.ClientIP(), ocpServerIp, resp.TraceId, duration, resp.Status, resp.Error.String())
}
c.JSON(resp.Status, resp)
}
}
func MonitorAgentPostHandler(c *gin.Context) {
startTime := time.Now()
c.Next()
duration := time.Now().Sub(startTime)
serverIp := c.Request.Header.Get(trace.OcpServerIpHeader)
ctx := NewContextWithTraceId(c)
fields := log.Fields{
"url": c.Request.URL,
"duration": duration,
"status": c.Writer.Status(),
"ocpServerIp": serverIp,
"client": c.ClientIP(),
}
if duration < 100*time.Millisecond {
fields[logrus.FieldKeyLevel] = logrus.DebugLevel
}
log.WithContext(ctx).WithFields(fields).Info("request end")
}
func HttpStatMiddleware(c *gin.Context) {
startTime := time.Now()
// run other middleware
c.Next()
stat.HttpRequestMillisecondsSummary.With(prom.Labels{
stat.HttpMethod: c.Request.Method,
stat.HttpStatus: fmt.Sprintf("%d", c.Writer.Status()),
stat.HttpApiPath: c.Request.URL.Path,
}).Observe(float64(time.Now().Sub(startTime)) / float64(time.Millisecond))
}
func Recovery(c *gin.Context, err interface{}) {
// log err to file
log.WithContext(NewContextWithTraceId(c)).Errorf("request context %+v, err:%+v", c, err)
}
func IgnoreFaviconHandler(c *gin.Context) {
if c.Request.URL.Path == "/favicon.ico" {
c.Abort()
}
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package web
package common
import (
"context"
"encoding/base64"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/config"
http2 "github.com/oceanbase/obagent/lib/http"
)
type Authorizer interface {
......@@ -28,8 +19,23 @@ type Authorizer interface {
SetConf(conf config.BasicAuthConfig)
}
func InitBasicAuthConf(ctx context.Context) {
httpAuthorizer = &BasicAuth{}
module := config.ManagerAgentBasicAuthConfigModule
err := config.InitModuleConfig(ctx, module)
if err != nil {
log.WithContext(ctx).Fatalf("init module %s config err:%+v", module, err)
}
log.WithContext(ctx).Infof("init module %s config end", module)
}
var httpAuthorizer Authorizer
func NotifyConf(conf config.BasicAuthConfig) {
httpAuthorizer.SetConf(conf)
}
type BasicAuth struct {
config config.BasicAuthConfig
}
......@@ -39,6 +45,9 @@ func (auth *BasicAuth) SetConf(conf config.BasicAuthConfig) {
}
func (auth *BasicAuth) Authorize(req *http.Request) error {
if !auth.config.MetricAuthEnabled && strings.HasPrefix(req.RequestURI, "/metrics/") {
return nil
}
// header: Authorization Basic base64-encoding-content
authHeader := req.Header.Get("Authorization")
authHeaders := strings.SplitN(authHeader, " ", 2)
......@@ -65,7 +74,24 @@ func (auth *BasicAuth) Authorize(req *http.Request) error {
return nil
}
log.Infof("auth:%+v", auth.config)
return errors.Errorf("auth failed for user: %s", contentStrs[0])
}
func AuthorizeMiddleware(c *gin.Context) {
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
if httpAuthorizer == nil {
ctxlog.Warnf("basic auth is nil, please check the initial process.")
c.Next()
return
}
err := httpAuthorizer.Authorize(c.Request)
if err != nil {
ctxlog.Errorf("basic auth Authorize failed, err:%+v", err)
c.Abort()
c.JSON(http.StatusUnauthorized, http2.BuildResponse(nil, err))
return
}
c.Next()
}
package common
import (
"net/http"
"github.com/felixge/fgprof"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
adapter "github.com/gwatts/gin-adapter"
)
func InitPprofRouter(r *gin.Engine) {
pprof.Register(r, "debug/pprof")
r.GET("/debug/fgprof", adapter.Wrap(func(_ http.Handler) http.Handler {
return fgprof.Handler()
}))
}
package mgragent
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/command"
)
var restartCmd = command.WrapFunc(func(taskToken agent.TaskToken) error {
log.Info("restarting agent")
cmd := agent.NewAgentctlCmd()
return cmd.Restart(taskToken)
})
func agentStatusService(c *gin.Context) {
log.Info("query agent status")
admin := agent.NewAdmin(agent.DefaultAdminConf())
status, err := admin.AgentStatus()
if err != nil {
common.SendResponse(c, nil, err)
return
}
common.SendResponse(c, status, nil)
}
package mgragent
import (
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/executor/file"
)
func isFileExists(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
var param file.GetFileExistsParam
c.BindJSON(&param)
data, err := file.IsFileExists(ctx, param)
common.SendResponse(c, data, err)
}
func getRealStaticPath(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
var param file.GetRealStaticPathParam
c.BindJSON(&param)
data, err := file.GetRealStaticPath(ctx, param)
common.SendResponse(c, data, err)
}
package mgragent
import (
"archive/zip"
"context"
"fmt"
"io"
"strconv"
"sync"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/executor/log_query"
)
// QueryLogRequest log query request params
type QueryLogRequest struct {
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
LogType string `json:"logType"`
Keyword []string `json:"keyword"`
KeywordType string `json:"keywordType"`
ExcludeKeyword []string `json:"excludeKeyword"`
ExcludeKeywordType string `json:"excludeKeywordType"`
LogLevel []string `json:"logLevel"`
ReqId string `json:"reqId"`
LastQueryFileId string `json:"lastQueryFileId"`
LastQueryFileOffset int64 `json:"lastQueryFileOffset"`
Limit int64 `json:"limit"`
}
// DownloadLogRequest log download request params
type DownloadLogRequest struct {
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
LogType []string `json:"logType"`
Keyword []string `json:"keyword"`
KeywordType string `json:"keywordType"`
ExcludeKeyword []string `json:"excludeKeyword"`
ExcludeKeywordType string `json:"excludeKeywordType"`
LogLevel []string `json:"logLevel"`
ReqId string `json:"reqId"`
}
type LogEntryResponse struct {
LogAt time.Time `json:"logAt"`
LogLine string `json:"logLine"`
LogLevel string `json:"logLevel"`
FileName string `json:"fileName"`
FileId string `json:"fileId"`
FileOffset int64 `json:"fileOffset"`
}
type QueryLogResponse struct {
LogEntries []LogEntryResponse `json:"logEntries"`
FileId string `json:"fileId"`
FileOffset int64 `json:"fileOffset"`
}
func queryLogHandler(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
ctxLog := log.WithContext(ctx)
var param QueryLogRequest
err := c.BindJSON(&param)
if err != nil {
ctxLog.WithError(err).Error("bindJson failed")
return
}
if log_query.GlobalLogQuerier.GetConf().QueryTimeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, log_query.GlobalLogQuerier.GetConf().QueryTimeout)
defer cancel()
}
ctxLog.WithField("param", param)
ctxLog.Info("invoke log query")
// Limit the maximum number of queries at a time
if param.Limit == 0 {
param.Limit = 200
}
logQueryReqParam, err := buildLogQueryReqParams(param)
if err != nil {
ctxLog.WithError(err).Error("buildLogQueryReqParams failed")
return
}
logEntryChan := make(chan log_query.LogEntry, 1)
logQuery, err := log_query.NewLogQuery(log_query.GlobalLogQuerier.GetConf(), logQueryReqParam, logEntryChan)
if err != nil {
ctxLog.WithError(err).Error("create NewLogQuery failed")
common.SendResponse(c, nil, err)
return
}
logEntries := make([]LogEntryResponse, 0)
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
for logEntry := range logEntryChan {
logEntries = append(logEntries, buildLogEntryResp(logEntry))
}
}()
lastPos, err := log_query.GlobalLogQuerier.Query(ctx, logQuery)
if err != nil {
ctxLog.WithError(err).Error("query failed")
common.SendResponse(c, nil, err)
return
}
wg.Wait()
resp := QueryLogResponse{
LogEntries: logEntries,
}
if lastPos != nil {
resp.FileId = fmt.Sprintf("%d", lastPos.FileId)
resp.FileOffset = lastPos.FileOffset
}
common.SendResponse(c, resp, nil)
}
func downloadLogHandler(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
ctxLog := log.WithContext(ctx)
var param DownloadLogRequest
err := c.BindJSON(&param)
if err != nil {
ctxLog.WithError(err).Error("bindJson failed")
return
}
if log_query.GlobalLogQuerier.GetConf().DownloadTimeout != 0 {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, log_query.GlobalLogQuerier.GetConf().DownloadTimeout)
defer cancel()
}
ctxLog.WithField("param", param)
ctxLog.Info("invoke log download")
w := c.Writer
zipWriter := zip.NewWriter(w)
prevLogFileName := ""
for _, logType := range param.LogType {
queryLogReq := QueryLogRequest{
StartTime: param.StartTime,
EndTime: param.EndTime,
LogType: logType,
Keyword: param.Keyword,
ExcludeKeyword: param.ExcludeKeyword,
LogLevel: param.LogLevel,
ReqId: param.ReqId,
}
logQueryReqParam, err := buildLogQueryReqParams(queryLogReq)
if err != nil {
ctxLog.WithError(err).Error("buildLogQueryReqParams failed")
return
}
logEntryChan := make(chan log_query.LogEntry, 1)
logQuery, err := log_query.NewLogQuery(log_query.GlobalLogQuerier.GetConf(), logQueryReqParam, logEntryChan)
if err != nil {
ctxLog.WithError(err).Error("create NewLogQuery failed")
common.SendResponse(c, nil, err)
return
}
wg := sync.WaitGroup{}
wg.Add(1)
go func() {
defer wg.Done()
var zipFileWriter io.Writer
for logEntry := range logEntryChan {
if logEntry.FileName != prevLogFileName {
zipFileWriter, err = zipWriter.Create(logEntry.FileName)
if err != nil {
ctxLog.WithField("logEntry", logEntry).WithError(err).Warn("zipFileWriter.Create failed")
}
prevLogFileName = logEntry.FileName
}
logLineBytes := append(logEntry.LogLine, '\n')
_, err1 := zipFileWriter.Write(logLineBytes)
if err1 != nil {
ctxLog.WithField("logEntry", logEntry).WithError(err1).Warn("write log entry bytes failed")
continue
}
}
}()
_, err = log_query.GlobalLogQuerier.Query(ctx, logQuery)
if err != nil {
ctxLog.WithError(err).Error("query failed")
common.SendResponse(c, nil, err)
return
}
wg.Wait()
}
err = zipWriter.Close()
if err != nil {
ctxLog.WithError(err).Error("close failed")
}
w.Flush()
common.SendResponse(c, "END-OF-STREAM", nil)
}
func buildLogQueryReqParams(req QueryLogRequest) (*log_query.QueryLogRequest, error) {
var (
lastQueryFileId uint64
err error
)
if req.LastQueryFileId != "" {
lastQueryFileId, err = strconv.ParseUint(req.LastQueryFileId, 10, 64)
if err != nil {
return nil, err
}
}
return &log_query.QueryLogRequest{
StartTime: req.StartTime,
EndTime: req.EndTime,
LogType: req.LogType,
Keyword: req.Keyword,
KeywordType: log_query.ConditionType(req.KeywordType),
ExcludeKeyword: req.ExcludeKeyword,
ExcludeKeywordType: log_query.ConditionType(req.ExcludeKeywordType),
LogLevel: req.LogLevel,
ReqId: req.ReqId,
LastQueryFileId: lastQueryFileId,
LastQueryFileOffset: req.LastQueryFileOffset,
Limit: req.Limit,
}, nil
}
func buildLogEntryResp(logEntry log_query.LogEntry) LogEntryResponse {
return LogEntryResponse{
LogAt: logEntry.LogAt,
LogLine: string(logEntry.LogLine),
LogLevel: logEntry.LogLevel,
FileName: logEntry.FileName,
FileId: fmt.Sprintf("%d", logEntry.FileId),
FileOffset: logEntry.FileOffset,
}
}
package mgragent
import (
"github.com/gin-gonic/gin"
adapter "github.com/gwatts/gin-adapter"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/errors"
"github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/stat"
)
func InitManagerAgentRoutes(s *http.StateHolder, r *gin.Engine) {
r.Use(common.HttpStatMiddleware)
// self stat metrics
r.GET("/metrics/stat", adapter.Wrap(stat.PromHandler))
r.Use(
gin.CustomRecovery(common.Recovery), // gin's crash-free middleware
common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"),
common.SetContentType,
common.PostHandlers("/debug/pprof"),
)
v1 := r.Group("/api/v1")
v1.GET("/time", common.TimeHandler)
v1.GET("/info", common.InfoHandler)
v1.GET("/git-info", common.GitInfoHandler)
v1.GET("/status", common.StatusHandler(s))
v1.POST("/status", common.StatusHandler(s))
// task routes
task := v1.Group("/task")
task.POST("/status", queryTaskHandler)
task.GET("/status", queryTaskHandler)
// agent admin routes
agent := v1.Group("/agent")
agent.POST("/status", agentStatusService)
agent.GET("/status", agentStatusService)
agent.POST("/restart", asyncCommandHandler(restartCmd))
// file routes
file := v1.Group("/file")
file.POST("/exists", isFileExists)
file.POST("/getRealPath", getRealStaticPath)
// system routes
system := v1.Group("/system")
system.POST("/hostInfo", getHostInfoHandler)
// module config
v1.POST("/module/config/update", common.UpdateConfigPropertiesHandler)
v1.POST("/module/config/notify", common.NotifyConfigPropertiesHandler)
v1.POST("/module/config/validate", common.ValidateConfigPropertiesHandler)
v1.GET("/module/config/status", common.ConfigStatusHandler)
v1.POST("/module/config/change", common.ChangeConfigHandler)
v1.POST("/module/config/reload", common.ReloadConfigHandler)
logGroup := v1.Group("/log")
logGroup.POST("/query", queryLogHandler)
logGroup.POST("/download", downloadLogHandler)
r.NoRoute(func(c *gin.Context) {
err := errors.Occur(errors.ErrBadRequest, "404 not found")
common.SendResponse(c, nil, err)
})
}
package mgragent
import (
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/executor/system"
)
func getHostInfoHandler(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
data, err := system.GetHostInfo(ctx)
common.SendResponse(c, data, err)
}
package mgragent
import (
"reflect"
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/errors"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/command"
path2 "github.com/oceanbase/obagent/lib/path"
)
type QueryTaskParam struct {
TaskToken string `json:"taskToken"`
}
type TaskTokenResult struct {
TaskToken string `json:"taskToken"`
}
type TaskStatusResult struct {
Finished bool `json:"finished"`
Ok bool `json:"ok"`
Result interface{} `json:"result"`
Err string `json:"err"`
Progress interface{} `json:"progress"`
}
var taskExecutor = command.NewExecutor(command.NewFileTaskStore(path2.TaskStoreDir()))
func queryTaskHandler(c *gin.Context) {
//ctx := NewContextWithTraceId(c)
var param QueryTaskParam
c.BindJSON(&param)
status, ok := taskExecutor.GetResult(command.ExecutionTokenFromString(param.TaskToken))
if !ok {
common.SendResponse(c, nil, errors.Occur(errors.ErrTaskNotFound, param.TaskToken))
return
}
common.SendResponse(c, TaskStatusResult{
Finished: status.Finished,
Ok: status.Ok,
Result: status.Result,
Err: status.Err,
Progress: status.Progress,
}, nil)
}
func TaskCount() int {
return len(taskExecutor.AllExecutions())
}
func asyncCommandHandler(task command.Command) gin.HandlerFunc {
return func(c *gin.Context) {
ctx := common.NewContextWithTraceId(c)
defaultParam := task.DefaultParam()
v := reflect.New(reflect.TypeOf(defaultParam))
v.Elem().Set(reflect.ValueOf(defaultParam))
param := v.Interface()
err := c.BindJSON(param)
if err != nil {
common.SendResponse(c, nil, err)
return
}
taskToken := param.(agent.TaskTokenParam)
if taskToken.GetTaskToken() == "" {
taskToken.SetTaskToken(command.GenerateTaskId())
}
input := command.NewInput(ctx, reflect.ValueOf(param).Elem().Interface())
input.WithRequestTaskToken(taskToken.GetTaskToken())
token, err := taskExecutor.Execute(task, input)
if err != nil {
common.SendResponse(c, nil, err)
return
}
common.SendResponse(c, TaskTokenResult{
TaskToken: token.String(),
}, nil)
}
}
package mgragent
import (
"context"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/command"
http2 "github.com/oceanbase/obagent/lib/http"
path2 "github.com/oceanbase/obagent/lib/path"
)
type S struct {
agent.TaskToken
A string
}
func TestAsyncCommandHandler(t *testing.T) {
os.MkdirAll(path2.TaskStoreDir(), 0755)
defer os.RemoveAll(path2.TaskStoreDir())
h := asyncCommandHandler(command.WrapFunc(func(s S) S {
s.A = s.A + s.A
return s
}))
req, _ := http.NewRequest("POST", "/xxx", strings.NewReader(`{"A":"a", "taskToken":"token12345"}`))
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = req
ctx.Keys = map[string]interface{}{common.TraceIdKey: "a"}
h(ctx)
resp := ctx.Keys[common.OcpAgentResponseKey].(http2.OcpAgentResponse)
if !resp.Successful || resp.Status != 200 {
t.Errorf("Fail %+v", resp)
return
}
tokenResult := resp.Data.(TaskTokenResult)
if tokenResult.TaskToken != "token12345" {
t.Errorf("bad result %+v", tokenResult)
return
}
result, ok := taskExecutor.WaitResult(command.ExecutionTokenFromString("token12345"))
if !ok {
t.Error("wait result failed")
return
}
s := result.Result.(S)
if s.A != "aa" {
t.Errorf("bad result %+v", s)
}
}
func TestAsyncCommandHandler2(t *testing.T) {
os.MkdirAll(path2.TaskStoreDir(), 0755)
defer os.RemoveAll(path2.TaskStoreDir())
h := asyncCommandHandler(command.WrapFunc(func(ctx context.Context, s S) (S, error) {
s.A = s.A + s.A
return s, nil
}))
req, _ := http.NewRequest("POST", "/xxx", strings.NewReader(`{"A":"a", "taskToken":"token12345"}`))
rec := httptest.NewRecorder()
ctx, _ := gin.CreateTestContext(rec)
ctx.Request = req
ctx.Keys = map[string]interface{}{common.TraceIdKey: "a"}
h(ctx)
resp := ctx.Keys[common.OcpAgentResponseKey].(http2.OcpAgentResponse)
if !resp.Successful || resp.Status != 200 {
t.Errorf("Fail %+v", resp)
return
}
tokenResult := resp.Data.(TaskTokenResult)
if tokenResult.TaskToken != "token12345" {
t.Errorf("bad result %+v", tokenResult)
return
}
result, ok := taskExecutor.WaitResult(command.ExecutionTokenFromString("token12345"))
if !ok {
t.Error("wait result failed")
return
}
s := result.Result.(S)
if s.A != "aa" {
t.Errorf("bad result %+v", s)
}
}
package monagent
import (
"context"
"net/http"
"os"
"github.com/gin-gonic/gin"
adapter "github.com/gwatts/gin-adapter"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/config"
http2 "github.com/oceanbase/obagent/lib/http"
"github.com/oceanbase/obagent/lib/system"
"github.com/oceanbase/obagent/stat"
)
func InitMonitorAgentRoutes(router *gin.Engine, localRouter *gin.Engine) {
router.GET("/metrics/stat", adapter.Wrap(stat.PromHandler))
v1 := router.Group("/api/v1")
v1.Use(common.PostHandlers())
v1.POST("/module/config/update", common.UpdateConfigPropertiesHandler)
v1.POST("/module/config/notify", common.NotifyConfigPropertiesHandler)
v1.POST("/module/config/validate", common.ValidateConfigPropertiesHandler)
v1.GET("/time", common.TimeHandler)
v1.GET("/info", common.InfoHandler)
v1.GET("/git-info", common.GitInfoHandler)
v1.POST("/status", monitorStatusHandler)
v1.GET("/status", monitorStatusHandler)
initMonagentLocalRoutes(localRouter)
}
func initMonagentLocalRoutes(localRouter *gin.Engine) {
common.InitPprofRouter(localRouter)
localRouter.GET("/metrics/stat", adapter.Wrap(stat.PromHandler))
group := localRouter.Group("/api/v1")
group.GET("/time", common.TimeHandler)
group.GET("/info", common.InfoHandler)
group.POST("/info", common.InfoHandler)
group.GET("/git-info", common.GitInfoHandler)
group.POST("/status", monitorStatusHandler)
group.GET("/status", monitorStatusHandler)
group.POST("/module/config/update", common.UpdateConfigPropertiesHandler)
group.POST("/module/config/notify", common.NotifyConfigPropertiesHandler)
}
func UseLocalMonitorMiddleware(r *gin.Engine) {
r.Use(
common.HttpStatMiddleware,
gin.CustomRecovery(common.Recovery), // gin's crash-free middleware
common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"),
common.PostHandlers("/debug/pprof", "/debug/fgprof", "/metrics/", "/api/v1/log/alarms"),
)
}
func UseMonitorMiddleware(r *gin.Engine) {
r.Use(
common.HttpStatMiddleware,
gin.CustomRecovery(common.Recovery), // gin's crash-free middleware
common.PreHandlers("/api/v1/module/config/update", "/api/v1/module/config/validate"),
common.MonitorAgentPostHandler,
)
}
func RegisterPipelineRoute(ctx context.Context, r *gin.Engine, url string, fh func(http.Handler) http.Handler) {
log.WithContext(ctx).Infof("register route %s", url)
r.GET(url, adapter.Wrap(fh))
}
var libProcess system.Process = system.ProcessImpl{}
func monitorStatusHandler(c *gin.Context) {
ports := make([]int, 0)
pid := os.Getpid()
processInfo, err := libProcess.GetProcessInfoByPid(int32(pid))
if err != nil {
log.Errorf("StatusHandler get processInfo failed, pid:%s", pid)
} else {
ports = processInfo.Ports
}
var info = http2.Status{
State: http2.Running,
Version: config.AgentVersion,
Pid: pid,
StartAt: common.StartAt,
Ports: ports,
}
common.SendResponse(c, info, nil)
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package route
import (
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/config"
)
// http handler: validate module config and save module config
func updateModuleConfigHandler(c *gin.Context) {
kvs := config.KeyValues{}
c.Bind(&kvs)
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
configVersion, err := config.UpdateConfig(ctx, &kvs, true)
if err != nil {
ctxlog.Errorf("update config err:%+v", err)
}
sendResponse(c, configVersion, err)
}
// http handler: notify module config
func notifyModuleConfigHandler(c *gin.Context) {
nconfig := new(config.NotifyModuleConfig)
c.Bind(nconfig)
ctx := NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx).WithFields(log.Fields{
"process": nconfig.Process,
"module": nconfig.Module,
"updated key values": nconfig.UpdatedKeyValues,
})
ctxlog.Debugf("notify module config")
err := config.NotifyModuleConfigForHttp(ctx, nconfig)
if err != nil {
ctxlog.Errorf("notify module config err:%+v", err)
}
sendResponse(c, "notify module config success", err)
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package route
import (
"net/http"
"github.com/gin-contrib/pprof"
"github.com/gin-gonic/gin"
adapter "github.com/gwatts/gin-adapter"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/stat"
)
func InitMonagentRoutes(r *gin.Engine) {
r.Use(
gin.Recovery(), // gin's crash-free middleware
)
v1 := r.Group("/api/v1")
v1.POST("/module/config/update", updateModuleConfigHandler)
v1.POST("/module/config/notify", notifyModuleConfigHandler)
metric := r.Group("/metrics")
metric.GET("/stat", adapter.Wrap(stat.PromGinWrapper))
}
func RegisterPipelineRoute(r *gin.Engine, url string, fh func(http.Handler) http.Handler) {
log.Infof("register route %s", url)
r.GET(url, adapter.Wrap(fh))
}
func InitPprofRouter(r *gin.Engine) {
pprof.Register(r, "debug/pprof")
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package web
import (
......@@ -23,9 +11,8 @@ import (
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/api/response"
"github.com/oceanbase/obagent/api/route"
server2 "github.com/oceanbase/obagent/api/server"
"github.com/oceanbase/obagent/api/common"
http2 "github.com/oceanbase/obagent/lib/http"
)
type HttpServer struct {
......@@ -34,22 +21,24 @@ type HttpServer struct {
// current session count, concurrent safely
Counter *Counter
// http routers
Router *gin.Engine
Router *gin.Engine
LocalRouter *gin.Engine
// address
Address string
// socket
Socket string
// http server, call its Run, Shutdown methods
Server *http.Server
Server *http.Server
LocalServer *http.Server
// stop the http.Server by calling cancel method
Cancel context.CancelFunc
// basic authorizer
BasicAuthorizer Authorizer
BasicAuthorizer common.Authorizer
}
func (server *HttpServer) AuthorizeMiddleware(c *gin.Context) {
ctx := route.NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx)
ctx := common.NewContextWithTraceId(c)
ctxlog := log.WithContext(ctx).WithField("url", c.Request.URL)
if server.BasicAuthorizer == nil {
ctxlog.Warnf("basic auth is nil, please check the initial process.")
c.Next()
......@@ -60,7 +49,7 @@ func (server *HttpServer) AuthorizeMiddleware(c *gin.Context) {
if err != nil {
ctxlog.Errorf("basic auth Authorize failed, err:%+v", err)
c.Abort()
c.JSON(http.StatusUnauthorized, response.BuildResponse(nil, err))
c.JSON(http.StatusUnauthorized, http2.BuildResponse(nil, err))
return
}
c.Next()
......@@ -81,13 +70,13 @@ func (server *HttpServer) UseBasicAuth() {
)
}
// Run start a httpServer
// run start a httpServer
// when ctx is cancelled, call shutdown to stop the httpServer
func (server *HttpServer) Run(ctx context.Context) {
server.Server.Handler = server.Router
if server.Address != "" {
tcpListener, err := server2.NewTcpListener(server.Address)
tcpListener, err := http2.NewTcpListener(server.Address)
if err != nil {
log.WithError(err).
Errorf("create tcp listener on address '%s' failed %v", server.Address, err)
......@@ -101,14 +90,19 @@ func (server *HttpServer) Run(ctx context.Context) {
}()
}
if server.Socket != "" {
socketListener, err := server2.NewSocketListener(server.Socket)
socketListener, err := http2.NewSocketListener(server.Socket)
if err != nil {
log.WithError(err).
Errorf("create socket listener on file '%s' failed %v", server.Socket, err)
return
}
go func() {
if err = server.Server.Serve(socketListener); err != nil {
server.LocalServer = &http.Server{
Handler: server.LocalRouter,
ReadTimeout: 5 * time.Minute,
WriteTimeout: 5 * time.Minute,
}
if err = server.LocalServer.Serve(socketListener); err != nil {
log.WithError(err).
Info("socket server exited")
}
......@@ -147,7 +141,7 @@ func (server *HttpServer) Shutdown(ctx context.Context) error {
func (server *HttpServer) counterPreHandlerFunc(c *gin.Context) {
if atomic.LoadInt32(&(server.Stopping)) == 1 {
c.Abort()
c.JSON(http.StatusServiceUnavailable, response.BuildResponse("server is shutdowning now.", nil))
c.JSON(http.StatusServiceUnavailable, http2.BuildResponse("server is shutdowning now.", nil))
return
}
......
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package web
import (
......
package web
import (
"context"
"net/http"
"os"
"sync"
"github.com/gin-gonic/gin"
"github.com/oceanbase/obagent/api/common"
monroute "github.com/oceanbase/obagent/api/monagent"
monconfig "github.com/oceanbase/obagent/config/monagent"
"github.com/oceanbase/obagent/executor/agent"
path2 "github.com/oceanbase/obagent/lib/path"
"github.com/oceanbase/obagent/monitor/engine"
)
var monitorAgentServer *MonitorAgentServer
func GetMonitorAgentServer() *MonitorAgentServer {
return monitorAgentServer
}
type MonitorAgentServer struct {
// original configs
Config *monconfig.MonitorAgentConfig
// sever of monitor metrics, selfstat, monitor manager API
Server *HttpServer
// two servers concurrent waitGroup
wg *sync.WaitGroup
}
// NewMonitorAgentServer init monagent server: init configs and logger, register routers
func NewMonitorAgentServer(conf *monconfig.MonitorAgentConfig) *MonitorAgentServer {
monagentServer := &MonitorAgentServer{
Config: conf,
Server: &HttpServer{
Counter: new(Counter),
Router: gin.New(),
LocalRouter: gin.New(),
BasicAuthorizer: new(common.BasicAuth),
Server: &http.Server{},
Address: conf.Server.Address,
Socket: agent.SocketPath(conf.Server.RunDir, path2.ProgramName(), os.Getpid()),
},
wg: &sync.WaitGroup{},
}
// register middleware before register handlers
monroute.UseMonitorMiddleware(monagentServer.Server.Router)
monroute.UseLocalMonitorMiddleware(monagentServer.Server.LocalRouter)
monitorAgentServer = monagentServer
return monitorAgentServer
}
// Run start monagent servers: admin server, monitor server
func (server *MonitorAgentServer) Run() {
server.wg.Add(1)
go func() {
defer server.wg.Done()
ctx, cancel := context.WithCancel(context.Background())
server.Server.Cancel = cancel
server.Server.Run(ctx)
}()
server.wg.Wait()
}
// registerRouter register routers such as adminServer router and monitor metrics router
func (server *MonitorAgentServer) RegisterRouter() {
server.wg.Add(1)
go func() {
defer server.wg.Done()
server.Server.UseCounter()
monroute.InitMonitorAgentRoutes(server.Server.Router, server.Server.LocalRouter)
for router := range engine.PipelineRouteChan {
monroute.RegisterPipelineRoute(router.Ctx, server.Server.Router, router.ExposeUrl, router.FuncHandler)
monroute.RegisterPipelineRoute(router.Ctx, server.Server.LocalRouter, router.ExposeUrl, router.FuncHandler)
}
}()
}
package web
import (
"context"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/oceanbase/obagent/config/monagent"
)
func TestMonitorAgentServerShutdown(t *testing.T) {
server := NewMonitorAgentServer(&monagent.MonitorAgentConfig{
Server: monagent.MonitorAgentHttpConfig{
Address: ":62889",
},
})
go server.Run()
t.Run("shutdown without any request", func(t *testing.T) {
err := server.Server.Shutdown(context.Background())
Convey("shutdown err", t, func() {
So(err, ShouldBeNil)
})
})
}
package web
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
. "github.com/smartystreets/goconvey/convey"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/mgragent"
"github.com/oceanbase/obagent/errors"
http2 "github.com/oceanbase/obagent/lib/http"
)
func Test_RouteHandler(t *testing.T) {
type args struct {
url string
}
type want struct {
successful bool
statusCode int
}
tests := []struct {
name string
args args
want want
}{
{
name: "example1",
args: args{
url: "http://127.0.0.1:62888/api/example/1",
},
want: want{
successful: true,
statusCode: http.StatusOK,
},
},
{
name: "example2",
args: args{
url: "http://127.0.0.1:62888/api/example/2",
},
want: want{
successful: true,
statusCode: http.StatusOK,
},
},
{
name: "example3",
args: args{
url: "http://127.0.0.1:62888/api/example/3",
},
want: want{
successful: true,
statusCode: http.StatusOK,
},
},
{
name: "example4",
args: args{
url: "http://127.0.0.1:62888/api/example/4",
},
want: want{
successful: false,
statusCode: http.StatusBadRequest,
},
},
}
server := NewServer(config.AgentVersion, mgragent.ServerConfig{})
InitExampleRoutes(server.Router)
handler := func(w http.ResponseWriter, r *http.Request) {
server.Router.ServeHTTP(w, r)
}
for _, tt := range tests {
Convey(tt.name, t, func() {
req := httptest.NewRequest("GET", tt.args.url, nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := ioutil.ReadAll(resp.Body)
var successResponse http2.OcpAgentResponse
_ = json.Unmarshal(body, &successResponse)
So(resp.StatusCode, ShouldEqual, tt.want.statusCode)
So(successResponse.Successful, ShouldEqual, tt.want.successful)
So(successResponse.Status, ShouldEqual, tt.want.statusCode)
})
}
}
// only for test
func InitExampleRoutes(r *gin.Engine) {
v1 := r.Group("/api/example")
v1.GET("/1", exampleHandler1)
v1.GET("/2", exampleHandler2)
v1.GET("/3", exampleHandler3)
v1.GET("/4", exampleHandler4)
}
var exampleHandler1 = func(c *gin.Context) {
data, err := singleExample()
sendResponse(c, data, err)
}
var exampleHandler2 = func(c *gin.Context) {
data, err := iterableExample()
sendResponse(c, data, err)
}
var exampleHandler3 = func(c *gin.Context) {
err := noDataExample()
sendResponse(c, nil, err)
}
var exampleHandler4 = func(c *gin.Context) {
data, err := errorExample()
sendResponse(c, data, err)
}
func singleExample() (string, *errors.OcpAgentError) {
return "this is data", nil
}
func iterableExample() ([]string, *errors.OcpAgentError) {
return []string{"data1", "data2", "data3"}, nil
}
func noDataExample() *errors.OcpAgentError {
return nil
}
func errorExample() (string, *errors.OcpAgentError) {
return "", errors.Occur(errors.ErrBadRequest)
}
func sendResponse(c *gin.Context, data interface{}, err error) {
resp := http2.BuildResponse(data, err)
c.Set(common.OcpAgentResponseKey, resp)
}
package web
import (
"context"
"net/http"
"os"
"sync"
"time"
"github.com/gin-gonic/gin"
log "github.com/sirupsen/logrus"
"github.com/oceanbase/obagent/api/common"
mgrroute "github.com/oceanbase/obagent/api/mgragent"
"github.com/oceanbase/obagent/config"
mgrconfig "github.com/oceanbase/obagent/config/mgragent"
"github.com/oceanbase/obagent/executor/agent"
http2 "github.com/oceanbase/obagent/lib/http"
path2 "github.com/oceanbase/obagent/lib/path"
)
type Server struct {
Config mgrconfig.ServerConfig
Router *gin.Engine
LocalRouter *gin.Engine
HttpServer *http.Server
LocalHttpServer *http.Server
state *http2.StateHolder
}
func NewServer(mode config.AgentMode, conf mgrconfig.ServerConfig) *Server {
if mode == config.DebugMode {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
router := gin.New()
localRouter := gin.New()
// TODO use gin.Logger() only for debugging, remove it before 3.2.0 publishes.
if mode == config.DebugMode {
router.Use(gin.Logger())
}
ret := &Server{
Router: router,
LocalRouter: localRouter,
Config: conf,
state: http2.NewStateHolder(http2.Running),
}
router.Use(common.IgnoreFaviconHandler)
router.Use(common.AuthorizeMiddleware)
mgrroute.InitManagerAgentRoutes(ret.state, router)
mgrroute.InitManagerAgentRoutes(ret.state, localRouter)
common.InitPprofRouter(localRouter)
return ret
}
func (s *Server) Run() {
s.HttpServer = &http.Server{
Handler: s.Router,
ReadTimeout: 60 * time.Minute,
WriteTimeout: 60 * time.Minute,
}
s.LocalHttpServer = &http.Server{
Handler: s.LocalRouter,
ReadTimeout: 60 * time.Minute,
WriteTimeout: 60 * time.Minute,
}
tcpListener, err := http2.NewTcpListener(s.Config.Address)
if err != nil {
log.WithError(err).Fatalf("create tcp listener on %s", s.Config.Address)
}
socketPath := agent.SocketPath(s.Config.RunDir, path2.ProgramName(), os.Getpid())
socketListener, err := http2.NewSocketListener(socketPath)
if err != nil {
log.WithError(err).Fatalf("create socket listener on %s", socketPath)
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
err = s.HttpServer.Serve(tcpListener)
if err != nil {
log.WithError(err).Fatal("serve on tcp listener failed")
}
wg.Done()
}()
go func() {
err = s.LocalHttpServer.Serve(socketListener)
if err != nil {
log.WithError(err).Fatal("serve on socket listener failed")
}
wg.Done()
}()
wg.Wait()
}
func (s *Server) Stop() {
err := s.HttpServer.Shutdown(context.Background())
log.WithError(err).Error("stop http server got error")
s.state.Set(http2.Stopped)
// TODO: wait command finished
for mgrroute.TaskCount() > 0 {
time.Sleep(time.Second)
}
}
func (s *Server) State() http2.State {
return s.state.Get()
}
package web
import (
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/mgragent"
http2 "github.com/oceanbase/obagent/lib/http"
)
func Test_NewServer(t *testing.T) {
Convey("time api", t, func() {
server := NewServer(config.AgentVersion, mgragent.ServerConfig{})
handler := func(w http.ResponseWriter, r *http.Request) {
server.Router.ServeHTTP(w, r)
}
req := httptest.NewRequest("GET", "http://127.0.0.1:62888/api/v1/time", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
body, _ := ioutil.ReadAll(resp.Body)
var successResponse http2.OcpAgentResponse
_ = json.Unmarshal(body, &successResponse)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
So(successResponse.Status, ShouldEqual, http.StatusOK)
So(successResponse.Successful, ShouldEqual, true)
})
}
{
"err.bad.request": "Bad request: %v",
"err.illegal.argument": "Illegal argument: %v",
"err.unexpected": "Unexpected error: %v",
"err.execute.command": "Execute shell command failed: %v",
"err.download.file": "Cannot download file from url: %s, reason: %s",
"err.invalid.checksum": "Invalid checksum",
"err.write.file": "Cannot write to file %v, reason: %v",
"err.find.file": "Cannot find file under %v, reason: %v",
"err.check.file.exists": "Cannot check whether file %v exists, reason: %v",
"err.create.directory": "Cannot create directory %v, reason: %v",
"err.remove.directory": "Cannot remove directory %v, reason: %v",
"err.chown.directory": "Cannot chown for directory %v, reason: %v",
"err.create.symlink": "Cannot create symbolic link from %v to %v, reason %v",
"err.process.cgroup": "Process cgroup failed: %v, reason: %v",
"err.task.not.found": "Task specified by token not found %v",
"err.query.package": "Query software package failed, reason: %v",
"err.install.package": "Install software package failed, reason: %v",
"err.uninstall.package": "Uninstall software package failed, reason: %v",
"err.extract.package": "Extract software package failed, reason: %v",
"err.check.process.exists": "Cannot check process existence of %v, reason: %v",
"err.get.process.info": "Cannot get process info of %v, reason: %v",
"err.stop.process": "Cannot stop process %v, reason: %v",
"err.get.process.proc": "Cannot get /proc/%v of process %v by user %v, reason: %v",
"err.system.disk.get.usage": "Cannot get disk usage of %v, reason: %v",
"err.system.disk.batch.get.disk.infos": "Cannot get disk infos, reason: %v",
"err.ob.install.pre-check": "Install OB pre-check failed: %v",
"err.ob.io.bench": "Do io bench failed: %v",
"err.observer.start": "Start observer failed: %v",
"err.check.observer.accessible": "Check observer accessible failed: %v",
"err.ob.bootstrap": "Bootstrap OB failed: %v",
"err.clean.ob.data.files": "Clean OB data files failed: %v",
"err.clean.ob.all.files": "Clean OB all files failed: %v",
"err.run.upgrade.script": "Failed to run upgrade script:'%v'",
"err.backup.start.backup.agent": "Start backup agent failed: %v",
"err.backup.stop.backup.agent": "Stop backup agent failed: %v",
"err.backup.check.backup.agent.online": "Check backup agent online failed: %v",
"err.backup.clean.backup.agent.files": "Clean backup agent files failed: %v",
"err.backup.clean.backup.data": "Clean backup data failed: %v",
"err.dump.backup.file": "Dump backup file %v failed: %v",
"err.backup.set.storage.client": "Set storage client failed,bucket: %v, reason: %v",
"err.backup.check.bucket": "check storage bucket failed, bucket: %v, reason: %v",
"err.backup.object.not.found": "object %v not found, bucket: %v, reason: %v",
"err.backup.get.storage.object": "get cos bucket %v object %v list failed, reason %v",
"err.backup.del.storage.object": "Delete cos bucket %v object %v failed, reason %v",
"err.agent.agentd.already.running": "agentd already running with pid file",
"err.agent.agentd.not.running": "agentd is not running",
"err.agent.agentd.exited.quickly": "start agentd failed, agentd exited quickly",
"err.monagent.pipeline.already.start": "monagent pipeling already start",
"err.monagent.pipeline.start.failed": "monagent pipeline start failed",
"err.monagent.remove.pipeline": "Remove monagent pipeline instance failed: %v",
"err.last.message.stub": "Last message stub"
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package bindata
import (
"bytes"
"compress/gzip"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info os.FileInfo
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
}
// Name return file name
func (fi bindataFileInfo) Name() string {
return fi.name
}
// Size return file size
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
// Mode return file mode
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
// ModTime return file modify time
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
// IsDir return file whether a directory
func (fi bindataFileInfo) IsDir() bool {
return fi.mode&os.ModeDir != 0
}
// Sys return file is sys mode
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _assetsI18nErrorEnJson = []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x96\x5d\x8e\xe3\x36\x0c\xc7\xdf\xe7\x14\xc4\x00\xc1\xbe\x6c\x75\x80\x79\xdc\x6e\x0b\x2c\x50\xa0\xe8\xc7\x1e\x40\x96\xe9\x44\xb0\x2c\x7a\x28\x29\x89\x51\xf4\xee\x0b\x49\x96\x62\x27\x76\x30\xfb\x32\x18\xfd\x49\xfe\x44\x91\x8c\xe4\xff\x5e\x00\x5e\x91\x59\x34\xb2\x15\x8c\xef\x01\x9d\x7f\x7d\x83\xd7\x2f\xb2\x85\x79\xf9\x06\x87\xf3\xeb\xe7\xe2\xa7\x8d\xc1\xa3\x34\x42\xf2\x31\x0c\x68\x93\xf3\xb7\xac\x41\xd1\xd6\x11\xc1\xe2\x75\x44\xe5\xb1\x8d\xbe\xdf\xeb\x0a\x90\x99\x78\xf6\x2d\xce\x2d\x5d\xac\x21\xd9\x8a\x4e\x1b\x8c\xfe\xbf\x4a\x6b\xc9\x43\xd1\x21\xea\xd0\x31\x0d\x10\xd8\xdc\xa5\x66\xcf\xd2\xe8\x56\xa8\x13\xaa\xde\x85\x21\xa5\x96\x35\xa8\x5a\xf5\xbe\xb0\xf6\x78\xbf\x4d\x12\xc1\x53\xde\xe6\x70\xfe\x0c\x8c\xd2\x91\x5d\x6f\xd4\x69\xfb\x90\x60\xd4\x72\x54\xb0\x2d\xf2\x6e\x6c\x4a\x24\x05\x0b\xbc\x6a\xe7\xdd\x82\x91\x6c\x70\x39\xa1\x3f\x21\x97\x14\x20\xbb\xdd\xd1\x2a\x8e\x51\x7a\x14\xad\x66\x54\x9e\x78\x5a\xd2\x92\x09\xaa\x69\x37\x25\xc6\x81\xce\xdb\x8c\x6c\xfa\x00\x43\x9d\xe8\x62\xb7\xd3\x88\x16\xe8\x88\x9f\x51\xee\x8e\xe3\xa6\xc1\x68\xdb\x3f\x1e\xc6\x4d\x43\x43\x46\x2b\x88\xe6\x3c\x06\x87\x73\xec\xd7\x8d\xb8\x06\x7a\xe9\x7a\x61\xc9\x8b\x8e\x82\x4d\x03\xf8\xaf\x74\x3d\xb8\x11\x95\xee\x34\xb6\xd0\x4c\xe0\xa9\x47\x0b\xa9\x89\xd1\x69\x0d\x78\x0f\xc8\x93\x18\xa5\xea\xe5\x31\xf5\xfb\xaf\x28\x80\xa3\xce\x5f\x24\x23\xcc\x16\xe8\xa4\x36\xd8\x6e\x17\x47\x5b\xe7\xa5\x31\x4b\xca\xb7\x2c\xfd\x1c\x27\xd8\x0d\xd2\xf7\x22\xfe\x1c\x0b\xaf\x9e\xa5\xf2\x4b\xd2\x6f\x59\xfa\x20\x67\x3d\xd0\x23\x93\x42\xe7\xf6\x66\x7a\x36\xe7\x59\x46\xab\x10\xa8\xdb\x9d\xa5\x23\xfa\xca\xd3\xb6\xa3\x05\xed\x88\xbe\xb2\xa2\xe9\x19\xc6\x79\x1a\x0b\x67\x81\x88\x72\x65\xec\x0e\xa2\x9b\x9c\xc7\x41\xb4\xda\xf5\x29\x9f\xe0\xe6\x22\x2d\x12\x89\x46\x48\x86\xcd\x34\x0a\x8a\x9a\xdb\x00\x30\xfe\x92\x0a\xb2\x1c\x81\x3f\xbf\x40\xd5\xe7\x52\xaf\x0f\x12\x01\x24\x1a\xb4\xea\x14\xe3\xbe\x12\x68\x82\xb4\xdc\x71\x77\xc8\x67\x8c\x05\x90\x9c\xee\xe7\x7f\xe2\x3f\x50\xf4\xcd\xa0\xdc\xc5\x1a\x2a\x55\x2c\x8f\x6e\xe6\x2b\x2e\xa5\x56\xe3\x6f\xc6\xbd\x74\x1b\x22\xef\x3c\xcb\x31\x3d\x25\x65\x11\x4f\xba\xb9\xb7\x41\x69\x63\x58\x2b\xbd\x4c\x57\x63\xee\x57\x94\x63\x4c\x94\xd3\x6d\xe8\x9e\x87\xc7\x0a\x3f\x46\xc7\x12\xef\x07\x73\xb0\x22\x8c\x47\x96\x2d\x0a\xa7\x58\x8f\xa9\x60\xbf\x27\xcf\x78\xaf\x70\xb0\x30\xdb\x21\xdb\xdf\x3e\x1d\xce\x9f\x16\xed\x6d\xa4\xea\xc3\x98\x8b\x5d\x16\xf2\x38\xbf\x8c\xb9\xf2\x59\x85\xa4\x6e\x66\x51\x19\x34\x6e\x20\x68\xfc\x30\x21\xb7\x71\x89\x10\x64\x8d\xb6\x8b\x36\xae\x50\xd9\xf8\x94\x98\xaa\xbb\x22\xde\xd5\x78\x9d\xdb\x6e\xa5\xb7\x78\xb1\xb3\x0f\xa0\xdc\xee\x25\xa1\x7e\x1c\x84\xa1\xd6\xa7\x3c\xbf\x5f\xc3\x50\xcb\x53\x5e\xcc\xcd\x60\x6a\x46\xa6\xeb\xf4\xf8\xab\x48\xf2\xce\x24\xe7\x10\xc6\xd1\x48\x85\x65\xdd\x2e\x26\xe5\xef\x6c\x82\x9b\xe9\x34\xcf\xc9\x53\x62\x9e\x96\x0d\xde\x2a\xa9\x44\x7b\x86\xc9\xc5\xbc\xeb\x47\x3d\xd1\x6e\x2b\x6a\x38\x8d\x93\x40\xaf\x84\x22\xeb\xd1\xce\xf7\x36\x8d\x13\xa0\x57\x50\xc4\xed\x72\xe6\x51\x48\x7f\x5b\x21\x0d\xa3\x6c\xa7\xf8\x6b\xb2\xda\x1e\x23\x26\x5b\x60\xb6\xc0\x6c\x81\x8b\xf6\x27\x18\x75\xfe\x54\xba\x65\xb4\xa2\xc5\xe7\xfa\x91\xa4\x5d\x7a\xa2\x8b\x61\x3b\x14\xaf\xda\x63\x2b\xde\x83\x56\xbd\x49\x5f\x21\xa9\xd2\x30\x33\xca\x43\x36\x2f\xb3\x37\x14\xef\xdb\xe1\x8c\x74\x5e\x0c\xe8\xe2\xdd\x2e\x9c\x0f\x4d\x24\xfd\x21\x9d\x87\x59\x84\x24\xbe\xfc\xff\xf2\x23\x00\x00\xff\xff\x72\x8b\x79\xab\x40\x0b\x00\x00")
func assetsI18nErrorEnJsonBytes() ([]byte, error) {
return bindataRead(
_assetsI18nErrorEnJson,
"assets/i18n/error/en.json",
)
}
func assetsI18nErrorEnJson() (*asset, error) {
bytes, err := assetsI18nErrorEnJsonBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{name: "assets/i18n/error/en.json", size: 2880, mode: os.FileMode(420), modTime: time.Unix(1630584784, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, fmt.Errorf("Asset %s not found", name)
}
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, fmt.Errorf("AssetInfo %s not found", name)
}
// AssetNames returns the names of the assets.
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
// _bindata is a table, holding each asset generator, mapped to its name.
var _bindata = map[string]func() (*asset, error){
"assets/i18n/error/en.json": assetsI18nErrorEnJson,
}
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
}
}
if node.Func != nil {
return nil, fmt.Errorf("Asset %s not found", name)
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{nil, map[string]*bintree{
"assets": {nil, map[string]*bintree{
"i18n": {nil, map[string]*bintree{
"error": {nil, map[string]*bintree{
"en.json": {assetsI18nErrorEnJson, map[string]*bintree{}},
}},
}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
if err != nil {
return err
}
return nil
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
}
package main
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"runtime"
"sync"
json "github.com/json-iterator/go"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/oceanbase/obagent/agentd/api"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/agentctl"
"github.com/oceanbase/obagent/config/sdk"
"github.com/oceanbase/obagent/executor/agent"
"github.com/oceanbase/obagent/lib/mask"
"github.com/oceanbase/obagent/lib/path"
"github.com/oceanbase/obagent/lib/trace"
agentlog "github.com/oceanbase/obagent/log"
)
const (
commandNameInfo = "info"
commandNameGitInfo = "git-info"
)
var (
agentCtlConfig *agentctl.AgentctlConfig
// root command
agentCtlCommand = &cobra.Command{
Use: "ob_agentctl",
Short: "ob_agentctl is a CLI for agent management.",
Long: `ob_agentctl is a command line tool for the agent. It provides operation, maintenance, and management functions for the agent.`,
PersistentPreRun: func(cmd *cobra.Command, args []string) {
if cmd.Use == commandNameInfo || cmd.Use == commandNameGitInfo {
return
}
ctx := trace.ContextWithRandomTraceId()
ctlConfig := cmd.Flag("config").Value.String()
var err error
agentCtlConfig, err = LoadConfig(ctlConfig)
if err != nil {
setResult(err)
os.Exit(1)
}
// Replace the home.path in the configuration file with the actual value
pathMap := map[string]string{"obagent.home.path": path.AgentDir()}
_, err = config.ReplaceConfValues(agentCtlConfig, pathMap)
if err != nil {
setResult(err)
os.Exit(1)
}
InitLog(agentCtlConfig.Log)
err = sdk.InitSDK(ctx, agentCtlConfig.SDKConfig)
if err != nil {
setResult(err)
os.Exit(1)
}
err = sdk.RegisterMgragentCallbacks(ctx)
if err != nil {
setResult(err)
os.Exit(1)
}
err = sdk.RegisterMonagentCallbacks(ctx)
if err != nil {
setResult(err)
os.Exit(1)
}
// Initialize sock proxy
if err := config.InitModuleConfig(ctx, config.ManagerAgentProxyConfigModule); err != nil {
setResult(err)
os.Exit(1)
}
},
}
setResultOnce sync.Once
)
// Defines the configuration of subcommands and command line parameters
func defineConfigCommands() {
// config command
configCommand := &cobra.Command{
Use: "config",
Short: "config management",
Long: "update key-value configs, save configs to config properties files, and notify configs to the module using these configs",
Example: "config --update key1=value1,key2=value2",
Run: func(cmd *cobra.Command, args []string) {
ctx := trace.ContextWithRandomTraceId()
// config meta
if err := config.InitModuleTypeConfig(ctx, config.ConfigMetaModuleType); err != nil {
log.WithContext(ctx).Fatal(err)
}
updateConfigs, err := cmd.Flags().GetStringSlice("update")
if err != nil {
setResult(err)
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --update err:%s", err)
}
notifyModules, err := cmd.Flags().GetStringSlice("notify")
if err != nil {
setResult(err)
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --notify err:%s", err)
}
validateConfigs, err := cmd.Flags().GetStringSlice("validate")
if err != nil {
setResult(err)
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config --validate err:%s", err)
}
log.WithContext(ctx).Infof("agentctl config updates:%+v, notify modules:%+v, validate configs:%+v", mask.MaskSlice(updateConfigs), notifyModules, mask.MaskSlice(validateConfigs))
err = runUpdateConfigs(ctx, updateConfigs)
if err != nil {
log.WithContext(ctx).WithField("updateConfigs", mask.MaskSlice(updateConfigs)).Errorf("agentctl config update config err:%s", err)
setResult(err)
}
err = runNotifyModules(ctx, notifyModules)
if err != nil {
log.WithContext(ctx).WithField("notifyModules", notifyModules).Errorf("agentctl config notify modules err:%s", err)
setResult(err)
}
err = runValidateConfigs(ctx, validateConfigs)
if err != nil {
log.WithContext(ctx).WithField("runValidateConfigs", mask.MaskSlice(validateConfigs)).Errorf("agentctl config validate err:%s", err)
}
setResult(err)
},
}
// update configuration: The key-value pair is used for update.
// You need to enter the complete configuration.
// The UPDATE verifies the configuration, saves the configuration,
// and notifies services to use the configuration.
configCommand.PersistentFlags().StringSliceP("update", "u", nil, "key-value pairs, e.g., key1=value1,key2=value2")
// Notification service configuration takes effect.
configCommand.PersistentFlags().StringSliceP("notify", "n", nil, "notify modules, e.g., mgragent.config,monagent")
// Verify that the configurations are consistent
configCommand.PersistentFlags().StringSliceP("validate", "v", nil, "validate config key-value pairs, e.g., key1=value1,key2=value2")
configNotifyCommand := &cobra.Command{
Use: "notify",
Short: "notify config change",
Long: "notify modules configs changes. omitting modules means notify all modules",
Example: "notify module1 module2",
Run: func(cmd *cobra.Command, args []string) {
ctx := trace.ContextWithRandomTraceId()
var err error
if len(args) > 0 {
err = config.NotifyModules(ctx, args)
} else {
err = config.NotifyAllModules(ctx)
}
if err != nil {
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config notify err:%s", err)
}
setResult(err)
},
}
configChangeCommand := &cobra.Command{
Use: "change",
Short: "change config properties",
Long: "change config properties",
Example: "change k1=v1 k2=v2",
Run: func(cmd *cobra.Command, args []string) {
ctx := trace.ContextWithRandomTraceId()
err := runUpdateConfigs(ctx, args)
if err != nil {
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config update err:%s", err)
}
setResult(err)
},
}
validateChangeCommand := &cobra.Command{
Use: "validate",
Short: "validate config properties",
Long: "validate config properties",
Example: "validate k1=v1 k2=v2",
Run: func(cmd *cobra.Command, args []string) {
ctx := trace.ContextWithRandomTraceId()
err := runValidateConfigs(ctx, args)
if err != nil {
log.WithContext(ctx).WithField("args", os.Args).Fatalf("agentctl config validate err:%s", err)
}
setResult(err)
},
}
configCommand.AddCommand(configChangeCommand, configNotifyCommand, validateChangeCommand)
agentCtlCommand.AddCommand(configCommand)
}
func adminConf() agent.AdminConf {
return agent.AdminConf{
RunDir: agentCtlConfig.RunDir,
LogDir: agentCtlConfig.LogDir,
ConfDir: agentCtlConfig.ConfDir,
BackupDir: agentCtlConfig.BackupDir,
TempDir: agentCtlConfig.TempDir,
PkgStoreDir: agentCtlConfig.PkgStoreDir,
TaskStoreDir: agentCtlConfig.TaskStoreDir,
AgentPkgName: agentCtlConfig.AgentPkgName,
PkgExt: agentCtlConfig.PkgExt,
StartWaitSeconds: 10,
StopWaitSeconds: 10,
AgentdPath: path.AgentdPath(),
}
}
func defineInfoCommands() {
agentCtlCommand.AddCommand(&cobra.Command{
Use: commandNameInfo,
Run: func(cmd *cobra.Command, args []string) {
onSuccess(config.GetAgentInfo())
},
})
agentCtlCommand.AddCommand(&cobra.Command{
Use: commandNameGitInfo,
Run: func(cmd *cobra.Command, args []string) {
onSuccess(config.GetGitInfo())
},
})
}
func defineOperationCommands() {
agentCtlCommand.AddCommand(&cobra.Command{
Use: "status",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
onError(errors.New("too many arguments"))
return
}
admin := agent.NewAdmin(adminConf())
status, err := admin.AgentStatus()
if err != nil {
onError(err)
} else {
onSuccess(status)
}
},
})
agentCtlCommand.AddCommand(&cobra.Command{
Use: "start",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
onError(errors.New("too many arguments"))
return
}
admin := agent.NewAdmin(adminConf())
err := admin.StartAgent()
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
})
stopCommand := &cobra.Command{
Use: "stop",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
onError(errors.New("too many arguments"))
return
}
taskToken := cmd.Flag("task-token").Value.String()
admin := agent.NewAdmin(adminConf())
err := admin.StopAgent(agent.TaskToken{TaskToken: taskToken})
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
}
stopCommand.PersistentFlags().String("task-token", "", "task token to store result")
agentCtlCommand.AddCommand(stopCommand)
serviceCommand := &cobra.Command{
Use: "service",
}
serviceStartCommand := &cobra.Command{
Use: "start",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
onError(errors.New("missing service name"))
return
}
name := args[0]
taskToken := cmd.Flag("task-token").Value.String()
admin := agent.NewAdmin(adminConf())
err := admin.StartService(agent.StartStopServiceParam{
TaskToken: agent.TaskToken{
TaskToken: taskToken,
},
StartStopAgentParam: api.StartStopAgentParam{
Service: name,
},
})
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
}
serviceStartCommand.PersistentFlags().String("task-token", "", "task token to store result")
serviceStopCommand := &cobra.Command{
Use: "stop",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 1 {
onError(errors.New("missing service name"))
return
}
name := args[0]
taskToken := cmd.Flag("task-token").Value.String()
admin := agent.NewAdmin(adminConf())
err := admin.StopService(agent.StartStopServiceParam{
TaskToken: agent.TaskToken{
TaskToken: taskToken,
},
StartStopAgentParam: api.StartStopAgentParam{
Service: name,
},
})
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
}
serviceStopCommand.PersistentFlags().String("task-token", "", "task token to store result")
serviceCommand.AddCommand(serviceStartCommand)
serviceCommand.AddCommand(serviceStopCommand)
agentCtlCommand.AddCommand(serviceCommand)
restartCommand := &cobra.Command{
Use: "restart",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
onError(errors.New("too many arguments"))
return
}
taskToken := cmd.Flag("task-token").Value.String()
admin := agent.NewAdmin(adminConf())
err := admin.RestartAgent(agent.TaskToken{TaskToken: taskToken})
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
}
restartCommand.PersistentFlags().String("task-token", "", "task token to store result")
agentCtlCommand.AddCommand(restartCommand)
reinstallCommand := &cobra.Command{
Use: "reinstall",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 0 {
onError(errors.New("too many arguments"))
return
}
taskToken := cmd.Flag("task-token").Value.String()
admin := agent.NewAdmin(adminConf())
source := cmd.Flag("source").Value.String()
checksum := cmd.Flag("checksum").Value.String()
version := cmd.Flag("version").Value.String()
// todo validate args
err := admin.ReinstallAgent(agent.ReinstallParam{
TaskToken: agent.TaskToken{TaskToken: taskToken},
DownloadParam: agent.DownloadParam{
Source: source,
Checksum: checksum,
Version: version,
},
})
if err != nil {
onError(err)
} else {
onSuccess("ok")
}
},
}
reinstallCommand.PersistentFlags().String("source", "", "package source")
reinstallCommand.PersistentFlags().String("checksum", "", "package checksum")
reinstallCommand.PersistentFlags().String("version", "", "package version")
reinstallCommand.PersistentFlags().String("task-token", "", "task token to store result")
agentCtlCommand.AddCommand(reinstallCommand)
}
func defineVersionCommands() {
agentCtlCommand.AddCommand(&cobra.Command{
Use: "version",
Run: func(cmd *cobra.Command, args []string) {
onSuccess(config.AgentVersion)
},
})
}
func main() {
runtime.GOMAXPROCS(1)
confPath := filepath.Join(path.ConfDir(), "agentctl.yaml")
agentCtlCommand.PersistentFlags().StringP("config", "c", confPath, "config file")
defineInfoCommands()
defineOperationCommands()
defineConfigCommands()
defineVersionCommands()
if err := agentCtlCommand.Execute(); err != nil {
log.WithField("args", os.Args).Fatal(err)
setResult(err)
}
}
func LoadConfig(configFile string) (*agentctl.AgentctlConfig, error) {
f, err := os.Open(configFile)
if err != nil {
return nil, err
}
ret := &agentctl.AgentctlConfig{
ConfDir: path.ConfDir(),
RunDir: path.RunDir(),
LogDir: path.LogDir(),
BackupDir: path.BackupDir(),
TempDir: path.TempDir(),
TaskStoreDir: path.TaskStoreDir(),
AgentPkgName: filepath.Join(path.AgentDir(), "obagent"),
PkgExt: "rpm",
PkgStoreDir: path.PkgStoreDir(),
}
err = yaml.NewDecoder(f).Decode(ret)
return ret, err
}
// init log
func InitLog(conf config.LogConfig) {
agentlog.InitLogger(agentlog.LoggerConfig{
Level: conf.Level,
Filename: conf.Filename,
MaxSize: conf.MaxSize,
MaxAge: conf.MaxAge,
MaxBackups: conf.MaxBackups,
LocalTime: conf.LocalTime,
Compress: conf.Compress,
})
}
// set the result to stdout
// logs will be written to log file
func setResult(err error) {
setResultOnce.Do(func() {
if err != nil {
onError(err)
return
}
onSuccess("success")
})
}
// the returned err will be written to stderr
func onError(err error) {
resp := &agent.AgentctlResponse{
Successful: false,
Error: err.Error(),
}
data, jsonerr := json.Marshal(resp)
if jsonerr != nil {
log.WithField("error", err).Errorf("json marshal err:%s", jsonerr)
fmt.Fprintf(os.Stderr, "%s", err.Error())
os.Exit(-1)
return
}
log.WithField("response", string(data)).Info("agentctl error")
fmt.Fprintf(os.Stderr, "%s", data)
os.Exit(-1)
}
// success message will be written to stdout
func onSuccess(message interface{}) {
resp := &agent.AgentctlResponse{
Successful: true,
Message: message,
}
data, jsonerr := json.Marshal(resp)
if jsonerr != nil {
log.WithField("message", message).Errorf("json marshal err:%s", jsonerr)
return
}
log.WithField("response", string(data)).Info("agentctl success")
fmt.Fprintf(os.Stdout, "%s", data)
}
func runAgentctl(configfile string) error {
return nil
}
// run validate commands: validate config is identical
func runNotifyModules(ctx context.Context, modules []string) error {
if len(modules) <= 0 {
return nil
}
return config.NotifyModules(ctx, modules)
}
// run config command: update module config
func runUpdateConfigs(ctx context.Context, pairs []string) error {
if len(pairs) <= 0 {
return nil
}
return config.UpdateConfigPairs(ctx, pairs)
}
func runValidateConfigs(ctx context.Context, pairs []string) error {
if len(pairs) <= 0 {
return nil
}
return config.ValidateConfigPairs(ctx, pairs)
}
package main
import (
"fmt"
"os"
"path/filepath"
"runtime"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"github.com/oceanbase/obagent/agentd"
config2 "github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/lib/path"
agentLog "github.com/oceanbase/obagent/log"
)
// command-line arguments
type arguments struct {
ConfigFile string
}
func main() {
runtime.GOMAXPROCS(1)
confPath := filepath.Join(path.ConfDir(), "/agentd.yaml")
rootCmd := &cobra.Command{
Use: "ob_agentd",
Short: "OB agent supervisor",
Long: "OB agentd is the daemon of ob-Agent. Responsible for starting and stopping," +
" guarding other agent processes, and status query.",
}
rootCmd.PersistentFlags().StringVarP(&confPath, "config", "c", confPath, "config file")
rootCmd.Run = func(cmd *cobra.Command, positionalArgs []string) {
run(confPath)
}
err := rootCmd.Execute()
if err != nil {
log.Error("start agentd failed: ", err)
os.Exit(-1)
}
}
func run(confPath string) {
config := loadConfig(confPath)
agentLog.InitLogger(agentLog.LoggerConfig{
Level: config.LogLevel,
Filename: filepath.Join(path.LogDir(), "agentd.log"), //fmt.Sprintf("agentd.%d.log", os.Getpid())),
MaxSize: 100 * 1024 * 1024,
MaxBackups: 10,
})
log.Infof("starting agentd with config %s", confPath)
// Replace the home.path in the configuration file with the actual value
pathMap := map[string]string{"obagent.home.path": path.AgentDir()}
_, err := config2.ReplaceConfValues(&config, pathMap)
if err != nil {
log.Errorf("start agentd with config file '%s' failed: %v", confPath, err)
os.Exit(-1)
return
}
watchdog := agentd.NewAgentd(config)
err = watchdog.Start()
if err != nil {
log.Errorf("start agentd with config file '%s' failed: %v", confPath, err)
os.Exit(-1)
return
}
watchdog.ListenSignal()
}
func loadConfig(confPath string) agentd.Config {
config := agentd.Config{
LogLevel: "info",
LogDir: "/tmp",
CleanupDangling: true,
}
confFile, err := os.Open(confPath)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "open config file %s failed: %v\n", confPath, err)
os.Exit(1)
return config
}
defer confFile.Close()
err = yaml.NewDecoder(confFile).Decode(&config)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "read config file %s failed: %v\n", confPath, err)
os.Exit(1)
return config
}
return config
}
package main
import (
"context"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/oceanbase/obagent/api/common"
"github.com/oceanbase/obagent/api/web"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/mgragent"
configsdk "github.com/oceanbase/obagent/config/sdk"
"github.com/oceanbase/obagent/lib/path"
"github.com/oceanbase/obagent/lib/shellf"
"github.com/oceanbase/obagent/lib/trace"
)
const (
ManagerPortKey = "ocp.agent.manager.http.port"
OcpAgentHomePath = "obagent.home.path"
)
// command-line arguments
type arguments struct {
ConfigFile string
}
func run(args arguments) {
ctx := trace.ContextWithRandomTraceId()
// Set the umask of the agent process to 0022 so that the operations
// of the agent are not affected by the umask set by the system
syscall.Umask(0022)
conf := mgragent.NewManagerAgentConfig(args.ConfigFile)
// Replace the home.path in the configuration file with the actual value
pathMap := map[string]string{OcpAgentHomePath: path.AgentDir()}
_, err := config.ReplaceConfValues(conf, pathMap)
if err != nil {
log.WithError(err).Fatalf("parse mgragent config file path fialed %s", err)
os.Exit(1)
}
// Initialize configuration
initModuleConfigs(ctx, conf.SDKConfig)
// Obtain the service startup port based on the port value in the config file
managerPort := config.GetConfigPropertiesByKey(ManagerPortKey)
portMap := map[string]string{ManagerPortKey: managerPort}
_, err = config.ReplaceConfValues(conf, portMap)
if err != nil {
log.WithError(err).Fatalf("parse mgragent config file port fialed %s", err)
os.Exit(1)
}
shellf.InitShelf(conf.ShellfConfig.TemplatePath)
log.WithContext(ctx).Infof("starting ocp manager agent, version %v", config.AgentVersion)
log.WithContext(ctx).Infof("agent running in %v mode", config.Mode)
server := web.NewServer(config.Mode, conf.Server)
go server.Run()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
select {
case sig := <-ch:
log.WithContext(ctx).Infof("signal '%s' received. exiting...", sig.String())
server.Stop()
}
}
func parseArgsAndRun() error {
var args arguments
rootCmd := &cobra.Command{
Use: "ob_mgragent",
Short: "OB manager agent",
Long: "OB manager agent is an operation and maintenance process of OB-Agent," +
" providing basic host operation and maintenance commands, OB operation and maintenance commands",
}
confPath := filepath.Join(path.ConfDir(), "mgragent.yaml")
rootCmd.PersistentFlags().StringVarP(&args.ConfigFile, "config", "c", confPath, "config file")
rootCmd.Run = func(cmd *cobra.Command, positionalArgs []string) {
// The startup phase is set to debug
log.SetLevel(log.DebugLevel)
run(args)
}
return rootCmd.Execute()
}
func main() {
runtime.GOMAXPROCS(1)
err := parseArgsAndRun()
if err != nil {
log.Fatal(err)
os.Exit(-1)
}
}
func initModuleConfigs(ctx context.Context, sdkconf config.SDKConfig) {
// Initialize the SDK
mgragent.GlobalConfigManager = mgragent.NewManager(mgragent.ManagerConfig{
ModuleConfigDir: sdkconf.ModuleConfigDir,
ConfigPropertiesDir: sdkconf.ConfigPropertiesDir,
})
err := configsdk.InitSDK(ctx, sdkconf)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
err = configsdk.RegisterMgragentCallbacks(ctx)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
err = configsdk.RegisterMonagentCallbacks(ctx)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
// Initialize the log
if err := config.InitModuleConfig(ctx, config.ManagerLogConfigModule); err != nil {
log.WithContext(ctx).Fatal(err)
}
// Initialize sock proxy
if err := config.InitModuleConfig(ctx, config.ManagerAgentProxyConfigModule); err != nil {
log.WithContext(ctx).Fatal(err)
}
// Self-monitoring configuration
if err := config.InitModuleTypeConfig(ctx, config.StatConfigModuleType); err != nil {
log.WithContext(ctx).Fatalf("init module type %s, err:%+v", config.StatConfigModuleType, err)
}
// basic authentication gets the configuration and initializes
common.InitBasicAuthConf(ctx)
//Initialize the ob_logcleaner
if err := config.InitModuleConfig(ctx, config.OBLogcleanerModule); err != nil {
log.WithContext(ctx).Fatal(err)
}
// Initialize the logQuerier
if err := config.InitModuleConfig(ctx, config.ManagerLogQuerierModule); err != nil {
log.WithContext(ctx).Fatal(err)
}
// config meta
err = config.InitModuleConfig(ctx, config.ManagerAgentConfigMetaModule)
if err != nil {
log.WithContext(ctx).Fatal(err)
}
}
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package main
import (
"context"
"os"
"os/signal"
"path/filepath"
"runtime"
"syscall"
"time"
"runtime/debug"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/oceanbase/obagent/api/web"
"github.com/oceanbase/obagent/config"
"github.com/oceanbase/obagent/config/monagent"
"github.com/oceanbase/obagent/config/sdk"
"github.com/oceanbase/obagent/engine"
agentlog "github.com/oceanbase/obagent/log"
_ "github.com/oceanbase/obagent/plugins/exporters"
_ "github.com/oceanbase/obagent/plugins/inputs"
_ "github.com/oceanbase/obagent/plugins/outputs"
_ "github.com/oceanbase/obagent/plugins/processors"
"github.com/oceanbase/obagent/lib/path"
"github.com/oceanbase/obagent/lib/trace"
"github.com/oceanbase/obagent/monitor/engine"
_ "github.com/oceanbase/obagent/monitor/plugins/exporters"
_ "github.com/oceanbase/obagent/monitor/plugins/inputs"
_ "github.com/oceanbase/obagent/monitor/plugins/outputs"
_ "github.com/oceanbase/obagent/monitor/plugins/processors"
)
const (
MonitorPortKey = "ocp.agent.monitor.http.port"
OcpAgentHomePath = "obagent.home.path"
)
var (
// root command
monagentCommand = &cobra.Command{
Use: "monagent",
Short: "monagent is a monitoring agent.",
Long: `monagent is a monitoring agent for gathering, processing and pushing monitor metrics.`,
Use: "ob_monagent",
Short: "ob_monagent is a monitoring agent.",
Long: `ob_monagent is a monitoring agent for gathering, processing and pushing monitor metrics.`,
Run: func(cmd *cobra.Command, args []string) {
// The startup phase is set to debug
log.SetLevel(log.DebugLevel)
err := runMonitorAgent()
if err != nil {
log.WithField("args", args).Errorf("monagent execute err:%s", err)
......@@ -50,10 +50,9 @@ var (
)
func init() {
debug.SetGCPercent(config.GCPercent)
confPath := filepath.Join(path.ConfDir(), "monagent.yaml")
// monagent server config file
monagentCommand.PersistentFlags().StringP("config", "c", "conf/monagent.yaml", "config file")
monagentCommand.PersistentFlags().StringP("config", "c", confPath, "config file")
// plugins config use dir, all yaml files in the dir will be used as plugin config file
monagentCommand.PersistentFlags().StringP("pipelines_config_dir", "d", "conf/monagent/pipelines", "monitor pipelines config file dir")
......@@ -62,29 +61,47 @@ func init() {
}
func main() {
runtime.GOMAXPROCS(1)
if err := monagentCommand.Execute(); err != nil {
log.WithField("args", os.Args).Errorf("monagentCommand Execute failed %s", err.Error())
os.Exit(-1)
}
}
func runMonitorAgent() error {
monagentConfig, err := config.DecodeMonitorAgentServerConfig(viper.GetString("config"))
monagentConfig, err := monagent.DecodeMonitorAgentServerConfig(viper.GetString("config"))
if err != nil {
return errors.Wrap(err, "read monitor agent server config")
}
// init log for monagent
agentlog.InitLogger(agentlog.LoggerConfig{
Level: monagentConfig.Log.Level,
Filename: monagentConfig.Log.Filename,
MaxSize: monagentConfig.Log.MaxSize,
MaxAge: monagentConfig.Log.MaxAge,
MaxBackups: monagentConfig.Log.MaxBackups,
LocalTime: monagentConfig.Log.LocalTime,
Compress: monagentConfig.Log.Compress,
// Replace the home.path in the configuration file with the actual value
contextMap := map[string]string{OcpAgentHomePath: path.AgentDir()}
_, err = config.ReplaceConfValues(monagentConfig, contextMap)
if err != nil {
return errors.Wrap(err, "Failed to parse config file path")
}
ctxlog := trace.ContextWithRandomTraceId()
err = sdk.InitSDK(ctxlog, config.SDKConfig{
ModuleConfigDir: monagentConfig.ModulePath,
ConfigPropertiesDir: monagentConfig.PropertiesPath,
CryptoPath: monagentConfig.CryptoPath,
CryptoMethod: monagentConfig.CryptoMethod,
})
log.WithContext(ctxlog).Infof("sdk inited")
if err != nil {
return errors.Wrap(err, "init config sdk")
}
// Obtain the service startup port based on the port value in the config file
monitorPort := config.GetConfigPropertiesByKey(MonitorPortKey)
portMap := map[string]string{MonitorPortKey: monitorPort}
_, err = config.ReplaceConfValues(monagentConfig, portMap)
if err != nil {
return errors.Wrap(err, "Failed to parse config file port")
}
monagentServer := engine.NewMonitorAgentServer(monagentConfig)
monagentServer := web.NewMonitorAgentServer(monagentConfig)
ctx, cancel := context.WithCancel(context.Background())
defer func() {
cancel()
......@@ -95,36 +112,45 @@ func runMonitorAgent() error {
engine.GetPipelineManager().Schedule(ctx)
engine.GetConfigManager().Schedule(ctx)
err = sdk.InitSDK(config.SDKConfig{
ModuleConfigDir: monagentConfig.ModulePath,
ConfigPropertiesDir: monagentConfig.PropertiesPath,
CryptoPath: monagentConfig.CryptoPath,
CryptoMethod: monagentConfig.CryptoMethod,
})
log.Infof("sdk inited")
if err != nil {
return errors.Wrap(err, "init config sdk")
}
err = sdk.RegisterMonagentCallbacks()
err = sdk.RegisterMonagentCallbacks(ctxlog)
if err != nil {
return errors.Wrap(err, "register monagent callbacks")
}
err = config.InitModuleTypeConfig(ctx, config.MonitorAdminBasicAuthModuleType)
err = config.InitModuleTypeConfig(ctx, config.MonitorServerBasicAuthModuleType)
err = config.InitModuleTypeConfig(ctx, config.MonitorPipelineModuleType)
if err != nil {
log.WithError(err).Errorf("init pipeline config, err:%+v", err)
// Initialize the log
if err := config.InitModuleTypeConfig(ctxlog, config.MonitorLogConfigModuleType); err != nil {
log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorLogConfigModuleType, err)
}
err = monagentServer.RegisterRouter()
if err != nil {
return errors.Wrap(err, "monitor agent server register route")
// Initialize self-monitoring
if err := config.InitModuleTypeConfig(ctxlog, config.StatConfigModuleType); err != nil {
log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.StatConfigModuleType, err)
}
// Initialize basic authentication
if err := config.InitModuleTypeConfig(ctxlog, config.MonitorServerBasicAuthModuleType); err != nil {
log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorServerBasicAuthModuleType, err)
}
// Initialize monitoring and collection pipeline configuration
if err := config.InitModuleTypeConfig(ctxlog, config.MonitorPipelineModuleType); err != nil {
log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.MonitorPipelineModuleType, err)
}
// Initialize meta config
if err := config.InitModuleConfig(ctx, config.MonitorAgentConfigMetaModule); err != nil {
log.WithContext(ctxlog).WithError(err).Errorf("init module type %s, err:%+v", config.ConfigMetaModuleType, err)
}
err = monagentServer.Run()
if err != nil {
return errors.Wrap(err, "start monitor agent server")
go func() {
monagentServer.RegisterRouter()
monagentServer.Run()
}()
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
select {
case sig := <-ch:
log.WithContext(ctx).Infof("signal '%s' received. exiting...", sig.String())
engine.GetPipelineManager().Stop(ctx)
monagentServer.Server.Cancel()
close(engine.PipelineRouteChan)
}
return nil
......
package agentctl
import "github.com/oceanbase/obagent/config"
// agentctl meta
type AgentctlConfig struct {
SDKConfig config.SDKConfig `yaml:"sdkConfig"`
// log config
Log config.LogConfig `yaml:"log"`
RunDir string `yaml:"runDir"`
ConfDir string `yaml:"confDir"`
LogDir string `yaml:"logDir"`
BackupDir string `yaml:"backupDir"`
TempDir string `yaml:"tempDir"`
TaskStoreDir string `yaml:"taskStoreDir"`
AgentPkgName string `yaml:"agentPkgName"`
PkgExt string `yaml:"pkgExt"`
PkgStoreDir string `yaml:"pkgStoreDir"`
}
此差异已折叠。
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package config
import (
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
// Copyright (c) 2021 OceanBase
// obagent is licensed under Mulan PSL v2.
// You can use this software according to the terms and conditions of the Mulan PSL v2.
// You may obtain a copy of Mulan PSL v2 at:
//
// http://license.coscl.org.cn/MulanPSL2
//
// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
package config
import (
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
package monagent
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPipelineModuleStatus_Validate(t *testing.T) {
var pms PipelineModuleStatus = "false"
assert.False(t, pms.Validate())
pms = "active"
assert.True(t, pms.Validate())
}
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册