Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3263093788 | ||
|
|
44762837ec | ||
|
|
bf45b72d6f | ||
|
|
2165d95937 | ||
|
|
9fd20de60a | ||
|
|
4a292ef4bb | ||
|
|
055a3477a5 | ||
|
|
ed314a3c05 | ||
|
|
a481e56dbc | ||
|
|
c45cfba0be | ||
|
|
529f6f695b | ||
|
|
f45e3534ad | ||
|
|
d0af74e974 | ||
|
|
01bbe22143 | ||
|
|
3e70ef9f72 | ||
|
|
1a1a25b3c7 | ||
|
|
62821836b6 | ||
|
|
8b5b345202 | ||
|
|
fbe6bda621 | ||
|
|
8b878b603a | ||
|
|
cec792f425 | ||
|
|
b00e96caa0 | ||
|
|
9c1aef946b | ||
|
|
15914748b9 | ||
|
|
fc631f7576 | ||
|
|
a2b83e2408 | ||
|
|
4222a3e71f | ||
|
|
6fbb696779 | ||
|
|
d169ba986d | ||
|
|
e7ce0403f1 | ||
|
|
e9840d6904 | ||
|
|
7c022ac307 | ||
|
|
2a8f4377ff | ||
|
|
c63a0a699d | ||
|
|
231912e617 | ||
|
|
2a86558335 | ||
|
|
f845494607 | ||
|
|
fd549832e6 | ||
|
|
af6e1c1cd4 | ||
|
|
a35532f8e7 | ||
|
|
5403b4699b | ||
|
|
98b97af791 | ||
|
|
20cd6e0f3a | ||
|
|
64388ae7b5 | ||
|
|
c02125f192 | ||
|
|
783b54e101 | ||
|
|
7a1f26ed80 | ||
|
|
1eb319915a | ||
|
|
f578d351ca | ||
|
|
a447918f54 | ||
|
|
668c1bcf7c | ||
|
|
74fcfc16f5 | ||
|
|
c4f38267e9 | ||
|
|
2acfd42255 | ||
|
|
20dc733332 | ||
|
|
311cf315de | ||
|
|
2889e27a02 | ||
|
|
7775f707b1 | ||
|
|
f7140382b4 | ||
|
|
7926287dd8 | ||
|
|
6cdeaf1c31 | ||
|
|
42ce278c36 | ||
|
|
ede4c9160d | ||
|
|
a948c11eb9 | ||
|
|
c33b6b9cd8 | ||
|
|
cf05ec7653 | ||
|
|
92fb34f579 | ||
|
|
a01cf0b232 | ||
|
|
69173cb7a1 | ||
|
|
fd060438bf | ||
|
|
12e00952fb | ||
|
|
cbad19a3f6 | ||
|
|
73814c500e | ||
|
|
bf18a739ae | ||
|
|
aca5946dc2 | ||
|
|
ce6e09b03b | ||
|
|
0ed5eddd9a | ||
|
|
6f28d923d2 | ||
|
|
eec9e06137 | ||
|
|
8674b502d5 | ||
|
|
36068feed9 | ||
|
|
2d5e542593 | ||
|
|
94d66a7919 | ||
|
|
c59b544a65 | ||
|
|
b455bf6daf | ||
|
|
0c9b560ca7 | ||
|
|
d43d2eedb2 | ||
|
|
96f2c8068b | ||
|
|
59f5a646c6 | ||
|
|
78ecd669b3 | ||
|
|
a5acd554ab | ||
|
|
d4385e6471 | ||
|
|
fd7e58c02c | ||
|
|
47004e2377 | ||
|
|
f262e55698 | ||
|
|
fc42a5b1f0 | ||
|
|
54eae08472 | ||
|
|
b54c1650cd | ||
|
|
2540bdd9b7 |
4
.dockerignore
Normal file
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/EdgeManager/
|
||||
!EdgeManager.php
|
||||
!composer*
|
||||
5
.env
@@ -15,3 +15,8 @@ VUE_APP_I18N_FALLBACK_LOCALE=en
|
||||
|
||||
# element 颜色
|
||||
VUE_APP_ELEMENT_COLOR=#409EFF
|
||||
VUE_APP_HSLSERVER_PASSWORD=123456
|
||||
|
||||
#==true出现下拉,数据来源于MES
|
||||
#==false 手动输入,以及不渲染mes的设备编码
|
||||
VUE_APP_CHOOSABLE=false
|
||||
3
.gitignore
vendored
@@ -21,4 +21,5 @@ yarn-error.log*
|
||||
*.sw?
|
||||
|
||||
/vendor
|
||||
/linux_x64
|
||||
/EdgeServer-net6.0-linux-x64
|
||||
/docs/book
|
||||
78
Dockerfile
@@ -1,35 +1,61 @@
|
||||
FROM ubuntu:22.04
|
||||
FROM alpine:3.15
|
||||
|
||||
ARG DEBIAN_FRONTEND=noninteractive
|
||||
ENV TZ=Asia/Shanghai
|
||||
# https://serverfault.com/a/683651
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
ADD https://php.hernandev.com/key/php-alpine.rsa.pub /etc/apk/keys/php-alpine.rsa.pub
|
||||
|
||||
RUN sed -i 's/archive.ubuntu.com/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
|
||||
&& apt update \
|
||||
&& apt install -y software-properties-common \
|
||||
&& add-apt-repository -y ppa:ondrej/php \
|
||||
&& sed -i 's/ppa.launchpadcontent.net/launchpad.proxy.ustclug.org/g' /etc/apt/sources.list.d/ondrej-ubuntu-php-jammy.list \
|
||||
&& apt update \
|
||||
&& apt install -y --no-install-recommends \
|
||||
make \
|
||||
curl \
|
||||
RUN apk add ca-certificates \
|
||||
&& echo -e '\
|
||||
https://mirrors.ustc.edu.cn/alpine/latest-stable/main\n\
|
||||
https://mirrors.ustc.edu.cn/alpine/latest-stable/community\n\
|
||||
https://php.hernandev.com/v3.15/php-8.1\
|
||||
' > /etc/apk/repositories \
|
||||
&& apk update \
|
||||
&& apk add \
|
||||
tzdata \
|
||||
php \
|
||||
php-pear \
|
||||
php-dev \
|
||||
php-curl \
|
||||
php-xdebug \
|
||||
php-mbstring \
|
||||
php-xml \
|
||||
php-pgsql \
|
||||
# event扩展运行时需要
|
||||
php-sockets \
|
||||
# 以下两件为workerman运行时需要
|
||||
# https://github.com/walkor/workerman/issues/8
|
||||
php-posix \
|
||||
php-pcntl \
|
||||
# 安装event扩展需要pecl
|
||||
php-pear \
|
||||
# 提供phpize
|
||||
php-dev \
|
||||
# 编译event扩展需要
|
||||
autoconf \
|
||||
# 以下两件足以在alpine里提供一般C编译环境
|
||||
# g++则会提供gcc、musl-dev、libc-dev和g++,有点冗余
|
||||
gcc \
|
||||
musl-dev \
|
||||
libevent-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
make \
|
||||
composer \
|
||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone \
|
||||
&& ln -s /usr/bin/php8 /usr/bin/php
|
||||
|
||||
RUN printf "\n\n\n\n\nno\nyes\n\n" | pecl install event \
|
||||
&& printf "; priority=30\nextension=event.so\n" > /etc/php/$(ls /etc/php/)/mods-available/event.ini \
|
||||
&& phpenmod -v $(ls /etc/php/) event \
|
||||
&& curl -s https://mirrors.aliyun.com/composer/composer.phar -o /usr/local/bin/composer \
|
||||
&& chmod +x /usr/local/bin/composer \
|
||||
&& composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
|
||||
&& echo "extension=event.so" > /etc/php8/conf.d/02_event.ini
|
||||
|
||||
COPY . /EdgeManager
|
||||
|
||||
WORKDIR /EdgeManager
|
||||
|
||||
RUN composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/ \
|
||||
&& composer u --no-cache
|
||||
|
||||
RUN apk del \
|
||||
tzdata \
|
||||
composer \
|
||||
ca-certificates \
|
||||
php-pear \
|
||||
php-dev \
|
||||
autoconf \
|
||||
gcc \
|
||||
musl-dev \
|
||||
make \
|
||||
&& rm -rf /var/cache/apk/* \
|
||||
/etc/apk/keys/php-alpine.rsa.pub \
|
||||
/tmp/*
|
||||
|
||||
215
EdgeManager.php
@@ -7,21 +7,39 @@ use Workerman\Protocols\Http\Response;
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
|
||||
use EdgeManager\EDataCapture\{ EDataCapture, ENodeConfigure };
|
||||
use EdgeManager\EController\{ EConfigure, ECommand };
|
||||
|
||||
$options = getopt('h::', ['server_name:', 'user:', 'password:', 'help::']);
|
||||
$options = getopt('h::', ['no_dup_code', 'relay_device_status', 'server_name:', 'port::', 'user:', 'password:', 'worker_num::', 'help::']);
|
||||
|
||||
init_db($options['server_name'], $options['user'], $options['password']);
|
||||
if (array_key_exists('h', $options) or array_key_exists('help', $options)) {
|
||||
print_r(
|
||||
"EdgeManager使用说明:
|
||||
|
||||
--no_dup_code 禁止code在不同的working subclass间复用
|
||||
--relay_device_status 不判断是否是设备状态并转发到MES接口
|
||||
--server_name pg实例的FQDN
|
||||
--user pg实例的用户名
|
||||
--port pg实例的端口号
|
||||
--password pg实例的密码
|
||||
--worker_num 主程序进程数,默认为20个
|
||||
-h, --help 显示此帮助信息
|
||||
"
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
init_db($options['server_name'], $options['port'] ?? 5432, $options['user'], $options['password']);
|
||||
|
||||
$worker = new Worker('http://0.0.0.0:8888');
|
||||
$worker -> name = 'EntryPoint';
|
||||
$worker -> count = 20;
|
||||
$worker -> count = $options['worker_num'] ?? 20;
|
||||
|
||||
$worker -> onWorkerStart = function(Worker $worker) {
|
||||
global $options, $dbconn;
|
||||
|
||||
$dbconn = pg_connect(sprintf(
|
||||
"host=%s dbname=scada user=%s password=%s",
|
||||
$options['server_name'], $options['user'], $options['password']
|
||||
"host=%s port=%s dbname=scada user=%s password=%s",
|
||||
$options['server_name'], $options['port'] ?? 5432, $options['user'], $options['password']
|
||||
));
|
||||
};
|
||||
|
||||
@@ -31,16 +49,20 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
// 先预处理POST内容
|
||||
if ($request->header('content-type') === 'application/json') {
|
||||
$post = $request -> post();
|
||||
$get = $request -> get();
|
||||
if (isset($post['action'])) {
|
||||
$post = json_decode(json_encode($post, JSON_PRESERVE_ZERO_FRACTION));
|
||||
}
|
||||
} else {
|
||||
$get = $request -> get();
|
||||
$body = $request -> rawBody();
|
||||
if ($body === "") {
|
||||
$response = new Response(200, [
|
||||
'Content-Type' => 'application/json;charset=utf-8',
|
||||
], "空请求!");
|
||||
$connection -> send($response);
|
||||
if (count($get) === 0) {
|
||||
$response = new Response(200, [
|
||||
'Content-Type' => 'application/json;charset=utf-8',
|
||||
], "空请求!");
|
||||
$connection -> send($response);
|
||||
}
|
||||
} else {
|
||||
$post = json_decode($request -> rawBody());
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
@@ -79,7 +101,7 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
// 执行异步连接
|
||||
$task_connection->connect();
|
||||
} else {
|
||||
$enode_configure = new ENodeConfigure($dbconn, post: $post);
|
||||
$enode_configure = new ENodeConfigure($dbconn, no_dup_code: $options['no_dup_code'] ?? true, post: $post);
|
||||
$res = $enode_configure -> $action();
|
||||
|
||||
if ($res === true)
|
||||
@@ -93,16 +115,57 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'msg' => '服务器内部逻辑错误,请联系开发者!'
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
str_ends_with($post -> action, 'server')
|
||||
or str_ends_with($post -> action, 'device')
|
||||
) {
|
||||
$action = $post -> action;
|
||||
unset($post -> action);
|
||||
|
||||
if ($action === 'add_server') {
|
||||
$task_connection = new AsyncTcpConnection('Text://127.0.0.1:1888');
|
||||
$task_data = array(
|
||||
'action' => 'add_server',
|
||||
'data' => $post,
|
||||
);
|
||||
$task_connection -> send(json_encode($task_data));
|
||||
$task_connection -> onMessage = function(AsyncTcpConnection $task_connection, $task_result) use ($connection) {
|
||||
$task_connection -> close();
|
||||
$connection -> send($task_result);
|
||||
};
|
||||
// 执行异步连接
|
||||
$task_connection->connect();
|
||||
} else {
|
||||
$e_configure = new EConfigure($dbconn, post: $post);
|
||||
$res = $e_configure -> $action();
|
||||
|
||||
if ($res === true)
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'msg' => 'Success'
|
||||
)));
|
||||
else if ($res === "REMAINING") {
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '该服务中仍有关联的设备,不允许删除!'
|
||||
)));
|
||||
} else if ($res === false) {
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '服务器内部逻辑错误,请联系开发者!'
|
||||
)));
|
||||
}
|
||||
}
|
||||
} else if ($post -> action === 'set_node_data') {
|
||||
$data_capture = new EDataCapture($dbconn, post: $post);
|
||||
$data_capture = new EDataCapture($dbconn, no_dup_code: $options['no_dup_code'] ?? true, relay_device_status: $options['relay_device_status'] ?? true, post: $post);
|
||||
if ($data_capture -> check_res === 'WRONG_WORKING_SUBCLASS') {
|
||||
$response = new Response(200, [
|
||||
'Content-Type' => 'application/json;charset=utf-8',
|
||||
], json_encode(array(
|
||||
'action' => 'result_set_node_data',
|
||||
'errcode' => 4002,
|
||||
'errmsg' => '未登记过的工序单元!'
|
||||
'errmsg' => '工序单元有误,请检查是否未指定或未登记!'
|
||||
)));
|
||||
$connection -> send($response);
|
||||
} else if ($data_capture -> check_res === 'MISMATCH_TYPE') {
|
||||
@@ -114,6 +177,15 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'errmsg' => '节点编码和数值类型不匹配!'
|
||||
)));
|
||||
$connection -> send($response);
|
||||
} else if ($data_capture -> check_res === 'NO_DEVICE_CODE') {
|
||||
$response = new Response(200, [
|
||||
'Content-Type' => 'application/json;charset=utf-8',
|
||||
], json_encode(array(
|
||||
'action' => 'result_set_node_data',
|
||||
'errcode' => 4002,
|
||||
'errmsg' => 'device_code为必填字段!'
|
||||
)));
|
||||
$connection -> send($response);
|
||||
}
|
||||
$res = $data_capture -> set_node_data();
|
||||
if ($res === true) {
|
||||
@@ -129,6 +201,19 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'errmsg' => 'ROLLBACKed: Bad data received (structure and/or values)'
|
||||
)));
|
||||
}
|
||||
} else if ($post -> action === 'exec') {
|
||||
$task_connection = new AsyncTcpConnection('Text://127.0.0.1:1888');
|
||||
$task_data = array(
|
||||
'action' => 'exec',
|
||||
'data' => $post,
|
||||
);
|
||||
$task_connection -> send(json_encode($task_data));
|
||||
$task_connection -> onMessage = function(AsyncTcpConnection $task_connection, $task_result) use ($connection) {
|
||||
$task_connection -> close();
|
||||
$connection -> send($task_result);
|
||||
};
|
||||
// 执行异步连接
|
||||
$task_connection->connect();
|
||||
} else {
|
||||
// 有action,但是不知道是什么鬼动作
|
||||
$response = new Response(200, [
|
||||
@@ -153,23 +238,22 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
}
|
||||
}
|
||||
|
||||
$get = $request -> get();
|
||||
if (isset($get['query'])) {
|
||||
if ($get['query'] == 'nodes') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, get: $get);
|
||||
if ($get['query'] === 'nodes') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, no_dup_code: $options['no_dup_code'] ?? true, get: $get);
|
||||
$nodes = $enode_configure -> get_nodes();
|
||||
if (is_null($nodes))
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => 'no node data yet'
|
||||
'msg' => '未添加过节点!'
|
||||
)));
|
||||
else
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'data' => $nodes
|
||||
)));
|
||||
} else if ($get['query'] == 'working_subclasses') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, get: $get);
|
||||
} else if ($get['query'] === 'working_subclasses') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, no_dup_code: $options['no_dup_code'] ?? true, get: $get);
|
||||
$working_subclasses = $enode_configure -> get_working_subclasses($dbconn);
|
||||
if (is_null($working_subclasses))
|
||||
$connection -> send(json_encode(array(
|
||||
@@ -181,8 +265,8 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'code' => 0,
|
||||
'data' => $working_subclasses
|
||||
)));
|
||||
} else if ($get['query'] == 'codes') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, get: $get);
|
||||
} else if ($get['query'] === 'codes') {
|
||||
$enode_configure = new ENodeConfigure($dbconn, no_dup_code: $options['no_dup_code'] ?? true, get: $get);
|
||||
$codes = $enode_configure -> get_codes_by_working_subclasses();
|
||||
if (is_null($codes))
|
||||
$connection -> send(json_encode(array(
|
||||
@@ -194,8 +278,8 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'code' => 0,
|
||||
'data' => $codes
|
||||
)));
|
||||
} else if ($get['query'] == 'node_data') {
|
||||
$data_capture = new EDataCapture($dbconn, get: $get);
|
||||
} else if ($get['query'] === 'node_data') {
|
||||
$data_capture = new EDataCapture($dbconn, no_dup_code: $options['no_dup_code'] ?? true, get: $get);
|
||||
$data = $data_capture -> get_node_data();
|
||||
if (is_null($data))
|
||||
$connection -> send(json_encode(array(
|
||||
@@ -212,6 +296,44 @@ $worker -> onMessage = function(TcpConnection $connection, Request $request) {
|
||||
'code' => 0,
|
||||
'data' => $data
|
||||
)));
|
||||
} else if ($get['query'] === 'servers') {
|
||||
$servers = EConfigure::get_servers($dbconn);
|
||||
if (is_null($servers))
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '未添加过服务!'
|
||||
)));
|
||||
else
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'data' => $servers
|
||||
)));
|
||||
} else if ($get['query'] === 'device') {
|
||||
$e_configure = new EConfigure($dbconn, get: $get);
|
||||
$device = $e_configure -> get_device();
|
||||
if (is_null($device))
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '未添加过设备!'
|
||||
)));
|
||||
else
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'data' => $device
|
||||
)));
|
||||
} else if ($get['query'] === 'device_list') {
|
||||
$e_configure = new EConfigure($dbconn, get: $get);
|
||||
$devices = $e_configure -> get_device_list();
|
||||
if (is_null($devices))
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '未添加过设备!'
|
||||
)));
|
||||
else
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'data' => $devices
|
||||
)));
|
||||
} else {
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
@@ -228,8 +350,8 @@ $consumer -> onWorkerStart = function(Worker $consumer) {
|
||||
global $options, $consumer_dbconn;
|
||||
|
||||
$consumer_dbconn = pg_connect(sprintf(
|
||||
"host=%s dbname=scada user=%s password=%s",
|
||||
$options['server_name'], $options['user'], $options['password']
|
||||
"host=%s port=%s dbname=scada user=%s password=%s",
|
||||
$options['server_name'], $options['port'] ?? 5432, $options['user'], $options['password']
|
||||
));
|
||||
};
|
||||
|
||||
@@ -239,7 +361,7 @@ $consumer -> onMessage = function(TcpConnection $connection, $task_data) {
|
||||
$task_data = json_decode($task_data);
|
||||
|
||||
if ($task_data -> action === 'add_node') {
|
||||
$enode_configure = new ENodeConfigure($consumer_dbconn, post: $task_data -> data);
|
||||
$enode_configure = new ENodeConfigure($consumer_dbconn, no_dup_code: $options['no_dup_code'] ?? true, post: $task_data -> data);
|
||||
$res = $enode_configure -> add_node();
|
||||
|
||||
if ($res === true)
|
||||
@@ -250,7 +372,7 @@ $consumer -> onMessage = function(TcpConnection $connection, $task_data) {
|
||||
else if ($res === "REPLICATED")
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '同一工序单元内的节点编码不可重复!'
|
||||
'msg' => isset($options['no_dup_code']) ? '节点编码不可重复!' : '同一工序单元内的节点编码不可重复!'
|
||||
)));
|
||||
else if ($res === false) {
|
||||
$connection -> send(json_encode(array(
|
||||
@@ -258,6 +380,45 @@ $consumer -> onMessage = function(TcpConnection $connection, $task_data) {
|
||||
'msg' => '服务器内部逻辑错误,请联系开发者!'
|
||||
)));
|
||||
}
|
||||
} else if ($task_data -> action === 'add_server') {
|
||||
$e_configure = new EConfigure($consumer_dbconn, post: $task_data -> data);
|
||||
$res = $e_configure -> add_server();
|
||||
|
||||
if ($res === true)
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'msg' => 'Success'
|
||||
)));
|
||||
else if ($res === "REPLICATED")
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '该地址或名称已注册过服务!'
|
||||
)));
|
||||
else if ($res === false) {
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 1,
|
||||
'msg' => '服务器内部逻辑错误,请联系开发者!'
|
||||
)));
|
||||
}
|
||||
} else if ($task_data -> action === 'exec') {
|
||||
$e_command = new ECommand($consumer_dbconn, $task_data -> data);
|
||||
$e_command -> exec();
|
||||
|
||||
$connection -> send(json_encode(array(
|
||||
'code' => 0,
|
||||
'msg' => 'Success'
|
||||
)));
|
||||
} else if ($task_data -> action === 'set_device_status') {
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, 'http://8.sctmes.com:22347');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt($ch,CURLOPT_POSTFIELDS, json_encode($task_data) );
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
$return = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
79
EdgeManager/EController/ECommand.php
Normal file
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
namespace EdgeManager\EController;
|
||||
|
||||
class ECommand {
|
||||
function __construct(
|
||||
protected $dbconn,
|
||||
protected $post = NULL,
|
||||
) {}
|
||||
|
||||
function exec() {
|
||||
$command2API = [
|
||||
'server_restart' => '/Admin/ServerCloseAndRestart',
|
||||
'server_close' => '/Admin/ServerClose',
|
||||
'device_stop' => '/Edge/DeviceStopRequest',
|
||||
'device_continue' => '/Edge/DeviceContinueRequest'
|
||||
];
|
||||
|
||||
$res = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT url, port
|
||||
FROM hf_mes_scada_edgeserver_controller_server
|
||||
WHERE id = '%s'",
|
||||
$this -> post -> server_id
|
||||
));
|
||||
$server_info = pg_fetch_row($res);
|
||||
|
||||
if (str_starts_with($this -> post -> command, 'server')) {
|
||||
$ch = curl_init(
|
||||
$server_info[0]
|
||||
. ":"
|
||||
. $server_info[1]
|
||||
. $command2API[$this -> post -> command]
|
||||
);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
} else if (str_starts_with($this -> post -> command, 'device')) {
|
||||
$ch = curl_init(
|
||||
$server_info[0]
|
||||
. ":"
|
||||
. $server_info[1]
|
||||
. $command2API[$this -> post -> command]
|
||||
. '?data='
|
||||
. $this -> post -> device_name
|
||||
);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
|
||||
curl_setopt($ch, CURLOPT_HEADER, 1);
|
||||
curl_setopt(
|
||||
$ch,
|
||||
CURLOPT_USERPWD,
|
||||
$this -> post -> username
|
||||
. ":"
|
||||
. $this -> post -> password
|
||||
);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$return = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
if ($return) {
|
||||
pg_update(
|
||||
$this -> dbconn,
|
||||
"hf_mes_scada_edgeserver_controller_server",
|
||||
['updated' => false],
|
||||
['id' => $this -> post -> server_id]
|
||||
);
|
||||
}
|
||||
|
||||
pg_insert(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_command',
|
||||
[
|
||||
'server_id' => $this -> post -> server_id,
|
||||
'device_name' => $this -> post -> server_id ?? NULL,
|
||||
'command' => $this -> post -> command,
|
||||
'success' => $return
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
148
EdgeManager/EController/EConfigure.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
namespace EdgeManager\EController;
|
||||
|
||||
class EConfigure {
|
||||
function __construct(
|
||||
protected $dbconn,
|
||||
protected $post = NULL,
|
||||
protected $get = NULL,
|
||||
) {}
|
||||
|
||||
function add_server() {
|
||||
pg_query($this -> dbconn, "BEGIN");
|
||||
$addr_exists = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1 FROM hf_mes_scada_edgeserver_controller_server
|
||||
WHERE url = '%s'
|
||||
AND port = '%s'
|
||||
)",
|
||||
$this -> post -> url,
|
||||
$this -> post -> port
|
||||
));
|
||||
$name_exists = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1 FROM hf_mes_scada_edgeserver_controller_server
|
||||
WHERE name = '%s'
|
||||
)",
|
||||
$this -> post -> name
|
||||
));
|
||||
|
||||
if (
|
||||
pg_fetch_assoc($addr_exists)['exists'] === 't'
|
||||
or pg_fetch_assoc($name_exists)['exists'] === 't'
|
||||
) {
|
||||
pg_query($this -> dbconn, "ROLLBACK");
|
||||
return "REPLICATED";
|
||||
} else {
|
||||
$res = pg_insert(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_server',
|
||||
(array) $this -> post
|
||||
);
|
||||
if ($res) {
|
||||
pg_query($this -> dbconn, "COMMIT");
|
||||
return true;
|
||||
} else {
|
||||
pg_query($this -> dbconn, "ROLLBACK");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function remove_server() {
|
||||
$res = pg_delete(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_server',
|
||||
(array) $this -> post,
|
||||
);
|
||||
|
||||
return $res ? true : "REMAINING";
|
||||
}
|
||||
|
||||
function update_server() {
|
||||
return pg_update(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_server',
|
||||
(array) $this -> post,
|
||||
['id' => $this -> post -> id]
|
||||
);
|
||||
}
|
||||
|
||||
static function get_servers($dbconn) {
|
||||
$res = pg_query($dbconn, "SELECT * FROM hf_mes_scada_edgeserver_controller_server");
|
||||
return pg_fetch_all($res);
|
||||
}
|
||||
|
||||
function add_device() {
|
||||
$res = pg_insert(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_device',
|
||||
(array) $this -> post
|
||||
);
|
||||
|
||||
return $res === false ? $res : true;
|
||||
}
|
||||
|
||||
function remove_device() {
|
||||
return pg_delete(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_device',
|
||||
(array) $this -> post
|
||||
);
|
||||
}
|
||||
|
||||
function update_device() {
|
||||
pg_query($this -> dbconn, "BEGIN");
|
||||
|
||||
$res[] = pg_update(
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_edgeserver_controller_device',
|
||||
(array) $this -> post,
|
||||
['id' => $this -> post -> id]
|
||||
);
|
||||
|
||||
$res[] = $id_res = pg_query(
|
||||
$this -> dbconn, sprintf(
|
||||
"SELECT server_id
|
||||
FROM hf_mes_scada_edgeserver_controller_device
|
||||
WHERE id = '%s'
|
||||
LIMIT 1",
|
||||
$this -> post -> id
|
||||
)
|
||||
);
|
||||
$server_id = pg_fetch_row($id_res)[0];
|
||||
|
||||
$res[] = pg_update(
|
||||
$this -> dbconn,
|
||||
"hf_mes_scada_edgeserver_controller_server",
|
||||
['updated' => true],
|
||||
['id' => $server_id]
|
||||
);
|
||||
|
||||
if (in_array(false, $res)) {
|
||||
pg_query($this -> dbconn, "ROLLBACK");
|
||||
return false;
|
||||
} else {
|
||||
pg_query($this -> dbconn, "COMMIT");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function get_device() {
|
||||
$res = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT * FROM hf_mes_scada_edgeserver_controller_device
|
||||
WHERE id = '%s'",
|
||||
$this -> get['id']
|
||||
));
|
||||
return pg_fetch_all($res);
|
||||
}
|
||||
|
||||
function get_device_list() {
|
||||
$res = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT id, name FROM hf_mes_scada_edgeserver_controller_device
|
||||
WHERE server_id = '%s'",
|
||||
$this -> get['id']
|
||||
));
|
||||
return pg_fetch_all($res);
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,123 @@
|
||||
<?php
|
||||
namespace EdgeManager\EDataCapture;
|
||||
use \Workerman\Connection\AsyncTcpConnection;
|
||||
|
||||
class EDataCapture {
|
||||
function __construct(
|
||||
protected $dbconn,
|
||||
protected $no_dup_code,
|
||||
protected $relay_device_status,
|
||||
protected $post = NULL,
|
||||
protected $get = NULL,
|
||||
public $check_res = NULL,
|
||||
protected $working_subclass = NULL,
|
||||
protected $code_type = [],
|
||||
protected $data = []
|
||||
protected $data = [],
|
||||
protected $status = array(
|
||||
1 => 'IDLE',
|
||||
2 => 'RUN',
|
||||
3 => 'FINISH',
|
||||
4 => 'TROUBLE',
|
||||
5 => 'PAUSE',
|
||||
6 => 'OFFLINE')
|
||||
) {
|
||||
if (!is_null($this -> post)) {
|
||||
if (!in_array(
|
||||
$this -> post -> param -> working_subclass,
|
||||
ENodeConfigure::get_working_subclasses($this -> dbconn)
|
||||
)) {
|
||||
$this -> check_res = 'WRONG_WORKING_SUBCLASS';
|
||||
return;
|
||||
} else {
|
||||
$this -> working_subclass = $this -> post -> param -> working_subclass;
|
||||
if(isset($this -> post -> param -> working_subclass)){
|
||||
if (!in_array(
|
||||
$this -> post -> param -> working_subclass,
|
||||
ENodeConfigure::get_working_subclasses($this -> dbconn)
|
||||
)) {
|
||||
$this -> check_res = 'WRONG_WORKING_SUBCLASS';
|
||||
return;
|
||||
} else {
|
||||
$this -> working_subclass = $this -> post -> param -> working_subclass;
|
||||
}
|
||||
}else{
|
||||
if ($this -> no_dup_code) {
|
||||
$this -> check_res = 'WRONG_WORKING_SUBCLASS';
|
||||
return;
|
||||
}
|
||||
$working_subclass = ENodeConfigure::get_working_subclasses_by_codes($this -> dbconn, $this -> post -> param -> data[0] -> code);
|
||||
$this -> working_subclass = $working_subclass['working_subclass'];
|
||||
}
|
||||
|
||||
$res = pg_fetch_all(pg_query($this -> dbconn, sprintf(
|
||||
"SELECT code, type
|
||||
"SELECT code, type, category
|
||||
FROM hf_mes_scada_data_capture_node_configure
|
||||
WHERE working_subclass = '%s'",
|
||||
$this -> post -> param -> working_subclass
|
||||
$this -> working_subclass
|
||||
)));
|
||||
|
||||
|
||||
$code_type = &$this -> code_type;
|
||||
array_walk($res, function(&$v, $k) use (&$code_type) {
|
||||
$code_type[$v['code']] = $v['type'];
|
||||
array_walk($res, function($v, $k) use (&$code_type) {
|
||||
$code_type[$v['code']] = array('type' => $v['type'], 'category' => $v['category']);
|
||||
});
|
||||
|
||||
foreach ($this -> post -> param -> data as &$row) {
|
||||
if (($code_type[$row -> code]) === 'float' and is_int($row -> value))
|
||||
if (($code_type[$row -> code]['type']) === 'float' and is_int($row -> value))
|
||||
$row -> value = (float) number_format((float) $row -> value, 1, '.', '');
|
||||
$check_func = 'is_' . $code_type[$row -> code];
|
||||
$check_func = 'is_' . $code_type[$row -> code]['type'];
|
||||
if (!$check_func($row -> value)) {
|
||||
$this -> check_res = 'MISMATCH_TYPE';
|
||||
return;
|
||||
} else {
|
||||
$this -> data[$row -> code][] = [
|
||||
'value' => $row -> value,
|
||||
'device_code' => $row -> device_code ?? NULL,
|
||||
'batch' => $row -> batch ?? NULL
|
||||
];
|
||||
}
|
||||
|
||||
if (!isset($row -> device_code)) {
|
||||
$this -> check_res = 'NO_DEVICE_CODE';
|
||||
return;
|
||||
}
|
||||
if($this -> relay_device_status && $code_type[$row -> code]['category'] === 'DEVICE_STATUS'){
|
||||
if(!is_int($row -> value) && !in_array($row -> value , $this->status)){
|
||||
$this -> check_res = 'MISMATCH_TYPE';
|
||||
return;
|
||||
}
|
||||
$task_data = array(
|
||||
'action' => 'set_device_status',
|
||||
'param' => array(
|
||||
"device_code" => $row -> parent_device_code,
|
||||
"status" => is_int($row -> value) ? $this->status[$row -> value] : $row -> value,
|
||||
"errcode" => $row -> errcode ?? NULL,
|
||||
"msg" => $row -> msg ?? NULL,
|
||||
),
|
||||
);
|
||||
$task_connection = new AsyncTcpConnection('Text://127.0.0.1:1888');
|
||||
|
||||
$task_connection -> send(json_encode($task_data));
|
||||
$task_connection -> onMessage = function(AsyncTcpConnection $task_connection, $task_result){
|
||||
$task_connection -> close();
|
||||
};
|
||||
// 执行异步连接
|
||||
$task_connection->connect();
|
||||
}
|
||||
|
||||
$this -> data[$code_type[$row -> code]['type']][] = [
|
||||
'code' => $row -> code,
|
||||
'value' => $row -> value,
|
||||
'device_code' => $row -> device_code,
|
||||
'parent_device_code' => $row -> parent_device_code ?? NULL,
|
||||
'batch' => $row -> batch ?? NULL
|
||||
];
|
||||
}
|
||||
unset($row);
|
||||
}
|
||||
}
|
||||
|
||||
function set_node_data() {
|
||||
pg_query($this -> dbconn, "BEGIN");
|
||||
foreach ($this -> data as $code => $value) {
|
||||
foreach ($this -> data as $type => $value) {
|
||||
foreach (array_chunk($value, 6710885, true) as $chunk) {
|
||||
$sql_head = sprintf(
|
||||
'INSERT INTO "hf_mes_scada_data_capture_node_data_%s" (code, v_%s, device_code, batch)
|
||||
VALUES',
|
||||
$this -> working_subclass,
|
||||
$this -> code_type[$code]
|
||||
$type
|
||||
);
|
||||
foreach ($chunk as $row) {
|
||||
$sql_values[] = sprintf(
|
||||
"('%s', '%s', '%s', '%s')",
|
||||
$code,
|
||||
$row['code'],
|
||||
$row['value'] === false ? 'f' : $row['value'],
|
||||
$row['device_code'] ?? NULL,
|
||||
$row['device_code'],
|
||||
$row['parent_device_code'] ?? NULL,
|
||||
$row['batch'] ?? NULL
|
||||
);
|
||||
}
|
||||
@@ -78,14 +127,7 @@ class EDataCapture {
|
||||
unset($chunk);
|
||||
}
|
||||
unset($code, $value);
|
||||
|
||||
if (in_array(false, $res)) {
|
||||
pg_query($this -> dbconn, "ROLLBACK");
|
||||
return false;
|
||||
} else {
|
||||
pg_query($this -> dbconn, "COMMIT");
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function get_node_data() {
|
||||
|
||||
@@ -4,19 +4,30 @@ namespace EdgeManager\EDataCapture;
|
||||
class ENodeConfigure {
|
||||
function __construct(
|
||||
protected $dbconn,
|
||||
protected $no_dup_code,
|
||||
protected $post = NULL,
|
||||
protected $get = NULL
|
||||
protected $get = NULL,
|
||||
) {}
|
||||
|
||||
function add_node() {
|
||||
pg_query($this -> dbconn, "BEGIN");
|
||||
$exists = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM hf_mes_scada_data_capture_node_configure
|
||||
WHERE code = '%s'
|
||||
AND working_subclass = '%s'
|
||||
)", $this -> post -> code, $this -> post -> working_subclass
|
||||
));
|
||||
if ($this -> no_dup_code) {
|
||||
$exists = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM hf_mes_scada_data_capture_node_configure
|
||||
WHERE code = '%s'
|
||||
AND working_subclass = '%s'
|
||||
)", $this -> post -> code, $this -> post -> working_subclass
|
||||
));
|
||||
} else {
|
||||
$exists = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT EXISTS(
|
||||
SELECT 1 FROM hf_mes_scada_data_capture_node_configure
|
||||
WHERE code = '%s'
|
||||
)", $this -> post -> code
|
||||
));
|
||||
}
|
||||
|
||||
if (pg_fetch_assoc($exists)['exists'] === 't') {
|
||||
pg_query($this -> dbconn, "ROLLBACK");
|
||||
return "REPLICATED";
|
||||
@@ -27,12 +38,13 @@ class ENodeConfigure {
|
||||
$res[] = pg_query($this -> dbconn, sprintf(
|
||||
'CREATE TABLE IF NOT EXISTS "%s" (
|
||||
id serial8,
|
||||
code text,
|
||||
code text NOT NULL,
|
||||
v_string text,
|
||||
v_int int,
|
||||
v_float float8,
|
||||
v_bool bool,
|
||||
device_code text,
|
||||
device_code text NOT NULL,
|
||||
parent_device_code text,
|
||||
batch text,
|
||||
capture_time timestamp NOT NULL DEFAULT NOW()
|
||||
)', $table_name
|
||||
@@ -50,7 +62,18 @@ class ENodeConfigure {
|
||||
", $table_name
|
||||
));
|
||||
if (!in_array($table_name, pg_fetch_all_columns($table_exists, 1))) {
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT create_hypertable('\"%s\"', 'capture_time')", $table_name));
|
||||
$res[] = $node_count = pg_query($this -> dbconn, "SELECT COUNT(*) from timescaledb_information.data_nodes");
|
||||
$node_count = intval(pg_fetch_assoc($node_count)['count']);
|
||||
if ($node_count === 0) {
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT create_hypertable('\"%s\"', 'capture_time')", $table_name));
|
||||
} else {
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT create_distributed_hypertable('\"%s\"', 'capture_time', 'code', replication_factor => 3)", $table_name));
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT add_dimension('\"%s\"', 'device_code', number_partitions => %s)", $table_name, $node_count));
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT add_dimension('\"%s\"', 'parent_device_code', number_partitions => %s)", $table_name, $node_count));
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT add_dimension('\"%s\"', 'batch', number_partitions => %s)", $table_name, $node_count));
|
||||
$res[] = pg_query($this -> dbconn, sprintf("SELECT add_dimension('\"%s\"', 'v_bool', number_partitions => 2)", $table_name));
|
||||
}
|
||||
|
||||
$res[] = pg_query($this -> dbconn, sprintf(
|
||||
'CREATE INDEX ON "%s" (v_string, v_int, v_float, v_bool, capture_time DESC)
|
||||
WHERE COALESCE(v_string, v_int::text, v_float::text, v_bool::text) IS NOT NULL
|
||||
@@ -100,7 +123,7 @@ class ENodeConfigure {
|
||||
$this -> dbconn,
|
||||
'hf_mes_scada_data_capture_node_configure',
|
||||
(array) $this -> post,
|
||||
['code' => $this -> post -> code]
|
||||
['id' => $this -> post -> id]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,6 +137,16 @@ class ENodeConfigure {
|
||||
return pg_fetch_all_columns($res, 0);
|
||||
}
|
||||
|
||||
static function get_working_subclasses_by_codes($dbconn, $code) {
|
||||
$res = pg_query($dbconn, sprintf(
|
||||
"SELECT working_subclass
|
||||
FROM hf_mes_scada_data_capture_node_configure
|
||||
WHERE code = '%s'",
|
||||
$code
|
||||
));
|
||||
return pg_fetch_assoc($res);
|
||||
}
|
||||
|
||||
function get_codes_by_working_subclasses() {
|
||||
$res = pg_query($this -> dbconn, sprintf(
|
||||
"SELECT code
|
||||
|
||||
@@ -1,20 +1,38 @@
|
||||
<?php
|
||||
function init_db($server_name, $user, $password) {
|
||||
$dbconn = pg_connect(sprintf("host=%s user=%s password=%s", $server_name, $user, $password));
|
||||
function init_db($server_name, $port, $user, $password) {
|
||||
$dbconn = pg_connect(sprintf("host=%s port=%s user=%s password=%s", $server_name, $port, $user, $password));
|
||||
$res = pg_query($dbconn, "select datname from pg_database");
|
||||
|
||||
if (!in_array('scada', pg_fetch_all_columns($res, 0)))
|
||||
pg_query($dbconn, "CREATE DATABASE scada");
|
||||
pg_close($dbconn);
|
||||
|
||||
$dbconn = pg_connect(sprintf("host=%s dbname=scada user=%s password=%s", $server_name, $user, $password));
|
||||
$dbconn = pg_connect(sprintf("host=%s port=%s dbname=scada user=%s password=%s", $server_name, $port, $user, $password));
|
||||
// 全局未启用TSDB的时候在这里对scada数据库启用就好
|
||||
pg_query($dbconn, "CREATE EXTENSION IF NOT EXISTS timescaledb");
|
||||
pg_query(
|
||||
$dbconn,
|
||||
"DO $$ BEGIN
|
||||
CREATE TYPE category
|
||||
AS ENUM (
|
||||
'DEVICE_STATUS',
|
||||
'ITEM_ID',
|
||||
'DEVICE_DATA',
|
||||
'PROCESS_DATA',
|
||||
'RESULT_DATA',
|
||||
'STATISTICAL_DATA'
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object
|
||||
THEN null;
|
||||
END $$"
|
||||
);
|
||||
pg_query($dbconn, "CREATE TABLE IF NOT EXISTS hf_mes_scada_data_capture_node_configure (
|
||||
id serial2 primary key,
|
||||
code text,
|
||||
name text NOT NULL,
|
||||
type text NOT NULL,
|
||||
category category,
|
||||
flow_code text,
|
||||
working_subclass text NOT NULL,
|
||||
workstation text,
|
||||
@@ -25,20 +43,32 @@ function init_db($server_name, $user, $password) {
|
||||
id serial2 primary key,
|
||||
code text,
|
||||
name text NOT NULL,
|
||||
ip text NOT NULL,
|
||||
url text NOT NULL,
|
||||
port int2 NOT NULL,
|
||||
sync_addr text NOT NULL,
|
||||
address text NOT NULL,
|
||||
create_date timestamp NOT NULL DEFAULT NOW(),
|
||||
note text,
|
||||
updated bool NOT NULL DEFAULT false
|
||||
)");
|
||||
pg_query($dbconn, "CREATE TABLE IF NOT EXISTS hf_mes_scada_edgeserver_controller_device (
|
||||
id serial2 primary key,
|
||||
code text,
|
||||
name text NOT NULL,
|
||||
server_id serial2
|
||||
REFERENCES hf_mes_scada_edgeserver_controller_server(id),
|
||||
conf json,
|
||||
create_date timestamp NOT NULL DEFAULT NOW(),
|
||||
note text
|
||||
)");
|
||||
pg_query($dbconn, "CREATE TABLE IF NOT EXISTS hf_mes_scada_edgeserver_controller_command (
|
||||
id serial8 primary key,
|
||||
server_id serial2 references hf_mes_scada_edgeserver_controller_server(id),
|
||||
device_id text,
|
||||
node text,
|
||||
server_id serial2
|
||||
REFERENCES hf_mes_scada_edgeserver_controller_server(id)
|
||||
ON DELETE CASCADE,
|
||||
device_name text,
|
||||
command text,
|
||||
create_date timestamp NOT NULL DEFAULT NOW(),
|
||||
update_status bool
|
||||
success bool
|
||||
)");
|
||||
pg_close($dbconn);
|
||||
}
|
||||
|
||||
333
README.md
@@ -1,333 +0,0 @@
|
||||
# EdgeManager(SCADA系统)
|
||||
|
||||
- [EdgeManager(SCADA系统)](#edgemanagerscada系统)
|
||||
- [API](#api)
|
||||
- [class `EDataCapture`](#class-edatacapture)
|
||||
- [0. HTTP POST: `set_node_data`](#0-http-post-set_node_data)
|
||||
- [使用指北](#使用指北)
|
||||
- [后端](#后端)
|
||||
- [前端](#前端)
|
||||
- [开发环境](#开发环境)
|
||||
- [技术细节](#技术细节)
|
||||
- [0. `EdgeManager\EDataCapture\EDataCapture -> set_data()`为什么是以`6710885`为大小chunked的?](#0-edgemanageredatacaptureedatacapture---set_data为什么是以6710885为大小chunked的)
|
||||
|
||||
## API
|
||||
|
||||
> API设计最大程度遵循了已有的MES规范。
|
||||
|
||||
### class `EDataCapture`
|
||||
|
||||
#### 0. HTTP POST: `set_node_data`
|
||||
|
||||
**请求**
|
||||
|
||||
> 上位机程序中请单次传入尽量多的数据使得性能最大化。
|
||||
>
|
||||
> 但仍需兼顾传输速率和超时时间。
|
||||
>
|
||||
> 无需指定数值类型,服务端会自动根据已添加的节点信息检查,若不符则会报错。
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set_node_data",
|
||||
"param": {
|
||||
"working_subclass": <string>,
|
||||
"data": [
|
||||
{
|
||||
"code": <string>,
|
||||
// value的类型需与type对应
|
||||
"value": <string | int | float | bool>,
|
||||
"device_code": [string],
|
||||
"batch": [string]
|
||||
},
|
||||
{
|
||||
"code": <string>,
|
||||
"value": <string | int | float | bool>,
|
||||
"device_code": [string],
|
||||
"batch": [string]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**返回**
|
||||
|
||||
操作成功:
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "result_set_node_data",
|
||||
"errcode": 0,
|
||||
"errmsg": ""
|
||||
}
|
||||
```
|
||||
|
||||
操作失败(**工序单元尚未登记**):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "result_set_node_data",
|
||||
"errcode": 4002,
|
||||
"errmsg": "未登记过的工序单元!"
|
||||
}
|
||||
```
|
||||
|
||||
操作失败(**数值类型错误**):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "result_set_node_data",
|
||||
"errcode": 4002,
|
||||
"errmsg": "节点编码和数值类型不匹配!"
|
||||
}
|
||||
```
|
||||
|
||||
操作失败(不明原因):
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "result_set_node_data",
|
||||
"errcode": 4002,
|
||||
"errmsg": "ROLLBACKed: Bad data received (structure and/or values)"
|
||||
}
|
||||
```
|
||||
|
||||
## 使用指北
|
||||
|
||||
本项目可独立运行,也可作为MES的插件使用。
|
||||
|
||||
下文简要介绍如何将代码合并入MES中。
|
||||
|
||||
需要保证`timescaledb`的版本在**2.2.0或以上**。
|
||||
|
||||
### 后端
|
||||
|
||||
添加pg扩展(Ubuntu):
|
||||
|
||||
```bash
|
||||
sudo apt install php-pgsql
|
||||
```
|
||||
|
||||
添加pg扩展(CentOS 7):
|
||||
|
||||
```bash
|
||||
sudo yum install php-pgsql
|
||||
```
|
||||
|
||||
添加pg扩展(CentOS8):
|
||||
|
||||
```bash
|
||||
sudo dnf install php-pgsql
|
||||
```
|
||||
|
||||
目录结构:
|
||||
|
||||
```pre
|
||||
📦EdgeManager
|
||||
┣ 📂EDataCapture
|
||||
┃ ┣ 📜EDataCapture.php
|
||||
┃ ┗ 📜ENodeConfigure.php
|
||||
┣ 📜Init.php
|
||||
┗ 📜Utils.php
|
||||
```
|
||||
|
||||
建议使用composer autoload引入。
|
||||
|
||||
将目录`EdgeManager`复制至项目后在composer.json内写入:
|
||||
|
||||
```json
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"EdgeManager\\": "EdgeManager/"
|
||||
},
|
||||
"files": [
|
||||
"EdgeManager/Utils.php",
|
||||
"EdgeManager/Init.php"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
运行`composer dump-autoload`即可正常调用EdgeManager的代码。
|
||||
|
||||
### 前端
|
||||
|
||||
添加依赖(**使用npm**):
|
||||
|
||||
```bash
|
||||
npm i element-ui \
|
||||
@d2-projects/d2-crud \
|
||||
vue-cheetah-grid \
|
||||
@d2-projects/vue-table-export \
|
||||
@d2-projects/vue-table-import \
|
||||
github-markdown-css \
|
||||
marked@^2.0.0 \
|
||||
jschardet -S
|
||||
```
|
||||
|
||||
添加依赖(**使用yarn**):
|
||||
```bash
|
||||
yarn add element-ui \
|
||||
@d2-projects/d2-crud \
|
||||
vue-cheetah-grid \
|
||||
@d2-projects/vue-table-export \
|
||||
@d2-projects/vue-table-import \
|
||||
github-markdown-css \
|
||||
marked@^2.0.0 \
|
||||
jschardet
|
||||
```
|
||||
|
||||
需要在`main.js`中增加(全局引入组件):
|
||||
|
||||
```js
|
||||
// D2-Crud
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
import D2Crud from '@d2-projects/d2-crud'
|
||||
// Cheetah-Grid
|
||||
import vueCheetahGrid from 'vue-cheetah-grid'
|
||||
// 表格导出插件
|
||||
import pluginExport from '@d2-projects/vue-table-export'
|
||||
import pluginImport from '@d2-projects/vue-table-import'
|
||||
|
||||
Vue.use(ElementUI)
|
||||
Vue.use(D2Crud)
|
||||
Vue.use(vueCheetahGrid)
|
||||
Vue.use(pluginExport)
|
||||
Vue.use(pluginImport)
|
||||
```
|
||||
|
||||
*(可选)* 在`package.json`里更改`element-ui`版本:
|
||||
|
||||
```json
|
||||
"element-ui": ">2.15.9 || 2.15.8",
|
||||
```
|
||||
|
||||
`2.15.9`有一个小[bug](https://github.com/ElemeFE/element/issues/21941),会导致性能下降。
|
||||
|
||||
此外请参照目录结构中注释进行代码合并:
|
||||
|
||||
```pre
|
||||
📦src
|
||||
┣ 📂api
|
||||
┃ ┣ 📂modules
|
||||
┃ ┃ ┣ 📜scada.configure.api.js # 增添Axios请求
|
||||
┃ ┃ ┗ 📜sys.user.api.js
|
||||
┃ ┣ 📜index.js
|
||||
┃ ┣ 📜service.js
|
||||
┃ ┗ 📜tools.js
|
||||
┣ 📂assets
|
||||
┣ 📂components
|
||||
┃ ┣ 📂d2-markdown # 渲染markdown所需组件(在D2Admin的基础上精简了功能)
|
||||
┃ ┃ ┗ 📜index.vue
|
||||
┃ ┗ 📜index.js # 在此处注册d2-markdown
|
||||
┣ 📂libs
|
||||
┣ 📂locales
|
||||
┣ 📂menu
|
||||
┃ ┗ 📜index.js # 增添菜单
|
||||
┣ 📂plugin
|
||||
┣ 📂router
|
||||
┃ ┣ 📜index.js
|
||||
┃ ┗ 📜routes.js # 增添路由
|
||||
┣ 📂store
|
||||
┣ 📂views
|
||||
┃ ┣ 📂scada # 增添页面
|
||||
┃ ┃ ┣ 📂scadaConfigure
|
||||
┃ ┃ ┃ ┗ 📜index.vue
|
||||
┃ ┃ ┗ 📂scadaQuery
|
||||
┃ ┃ ┃ ┗ 📜index.vue
|
||||
┃ ┗ 📂system
|
||||
┣ 📜App.vue
|
||||
┣ 📜i18n.js
|
||||
┣ 📜main.js
|
||||
┗ 📜setting.js
|
||||
```
|
||||
|
||||
## 开发环境
|
||||
|
||||
拉取代码:
|
||||
|
||||
```bash
|
||||
# 建议先配置SSH key pair
|
||||
git clone ssh://git@118.195.187.246:10022/ysun/EdgeManager.git
|
||||
cd EdgeManager
|
||||
```
|
||||
|
||||
一键部署PHP workerman和TimescaleDB环境:
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
进入交互式Prompt:
|
||||
|
||||
```bash
|
||||
docker exec -it edge_manager bash
|
||||
```
|
||||
|
||||
后端调试:
|
||||
|
||||
```bash
|
||||
# In container
|
||||
php EdgeManager.php --server_name=GPU-server-01 --user=postgres --password=big_dick start
|
||||
```
|
||||
|
||||
前端调试:
|
||||
|
||||
```bash
|
||||
# In host
|
||||
# yarn
|
||||
# yarn watch
|
||||
yarn serve
|
||||
```
|
||||
|
||||
客户端连接:
|
||||
|
||||
```bash
|
||||
# sudo apt install postgresql-client
|
||||
# 登入
|
||||
psql -h localhost -U postgres
|
||||
# 显示数据库列表
|
||||
\l
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 0. `EdgeManager\EDataCapture\EDataCapture -> set_data()`为什么是以`6710885`为大小chunked的?
|
||||
|
||||
首先明确一点,根据PostgreSQL的技术架构,条件允许的情况下,**一次性插入多条记录是效率最高的**,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films (code, title, did, date_prod, kind) VALUES
|
||||
('B6717', 'Tampopo', 110, '1985-02-10', 'Comedy'),
|
||||
('HG120', 'The Dinner Game', 140, DEFAULT, 'Comedy');
|
||||
```
|
||||
|
||||
这其中,仅插入部分字段的值,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films (code, title, did, date_prod, kind)
|
||||
VALUES ('T_601', 'Yojimbo', 106, '1961-06-16', 'Drama');
|
||||
```
|
||||
|
||||
的效率又远远不如插入全部列,但使用特殊变量DEFAULT替代缺少的字段,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films VALUES
|
||||
('UA502', 'Bananas', DEFAULT, '1971-07-13', 'Comedy', '82 minutes');
|
||||
```
|
||||
|
||||
PostgreSQL的文档里没有提及这个话题,因为源码里为SQL语句的内存分配[指定了一个最大长度常量`MaxAllocSize`](https://github.com/postgres/postgres/blob/2373fe78dfc9d4aa2348a86fffdf8eb9d757e9d5/src/common/stringinfo.c#L28),其大小为比1GB小1字节。这个值可以在编译前手动修改,所以理论上每个PG运行的实例都可以不一样,如果我们使用的PG并非修改过源码手动编译的,那么SQL语句的最大长度为`1024 ** 3 - 1 = 1073741823` bytes。
|
||||
|
||||
> 安全起见,下述计算假定working_subclass等字段的最大长度均为我们以往约定的45 bytes。
|
||||
|
||||
而此处我源码中的SQL语句头长度为138 bytes(注意末尾空格):
|
||||
|
||||

|
||||
|
||||
单个语句体的长度为160 bytes:
|
||||
|
||||

|
||||
|
||||
所以其最多可以一次性插入`(1073741823 - 138) / 160`条记录,向下取整后即为`6710885`。
|
||||
|
||||
1
charts/.helmignore
Normal file
@@ -0,0 +1 @@
|
||||
*.png
|
||||
21
charts/Chart.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
apiVersion: v1
|
||||
name: timescaledb-multinode
|
||||
description: 'TimescaleDB Multinode Deployment.'
|
||||
version: 0.8.0
|
||||
# appVersion specifies the version of the software, which can vary wildly,
|
||||
# e.g. TimescaleDB 1.4.1 on PostgreSQL 11 or TimescaleDB 1.5.0 on PostgreSQL 12.
|
||||
# https://github.com/helm/helm/blob/master/docs/charts.md#the-appversion-field
|
||||
# To avoid confusion, we will not expose appVersion
|
||||
# appVersion: 0.0.1
|
||||
home: https://github.com/timescale/helm-charts
|
||||
sources:
|
||||
- https://github.com/timescale/helm-charts
|
||||
- https://github.com/timescale/timescaledb-docker-ha
|
||||
- https://github.com/zalando/patroni
|
||||
|
||||
# The chart is deprecated. We are looking for maintainers to remove this field.
|
||||
# More in https://github.com/timescale/helm-charts/blob/main/charts/timescaledb-multinode/README.md#call-for-maintainers
|
||||
deprecated: false
|
||||
134
charts/admin-guide.md
Normal file
@@ -0,0 +1,134 @@
|
||||
<!---
|
||||
This file and its contents are licensed under the Apache License 2.0.
|
||||
Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
-->
|
||||
|
||||
# TimescaleDB Single Administrator Guide
|
||||
|
||||
##### Table of Contents
|
||||
- [Configuration](#configuration)
|
||||
- [Backups](#backups)
|
||||
- [Cleanup](#cleanup)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
## Configuration
|
||||
The following table lists the configurable parameters of the TimescaleDB Helm chart and their default values.
|
||||
|
||||
| Parameter | Description | Default |
|
||||
|-----------------------------------|---------------------------------------------|-----------------------------------------------------|
|
||||
| `nameOverride` | Override the name of the chart | `timescaledb` |
|
||||
| `fullnameOverride` | Override the fullname of the chart | `nil` |
|
||||
| `replicaCount` | Amount of pods to spawn | `3` |
|
||||
| `image.repository` | The image to pull | `timescale/timescaledb-ha` |
|
||||
| `image.tag` | The version of the image to pull | `pg12.5-ts2.0.0-p0`
|
||||
| `image.pullPolicy` | The pull policy | `IfNotPresent` |
|
||||
| `credentials.accessNode.superuser`| Password of the superuser for the Access Node | `tea` |
|
||||
| `credentials.dataNode.superuser` | Password of the superuser for the Data Nodes | `coffee` |
|
||||
| `env` | Extra custom environment variables, expressed as [EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#envvarsource-v1-core) | `PGDATA` and some language settings |
|
||||
| `resources` | Any resources you wish to assign to the pod | `{}` |
|
||||
| `nodeSelector` | Node label to use for scheduling | `{}` |
|
||||
| `tolerations` | List of node taints to tolerate | `[]` |
|
||||
| `affinityTemplate` | A template string to use to generate the affinity settings | Anti-affinity preferred on hostname |
|
||||
| `affinity` | Affinity settings. Overrides `affinityTemplate` if set. | `{}` |
|
||||
| `postgresql.databases` | List of databases to automatically create a multinode setup for | `["postgres", "example"]` |
|
||||
| `postgresql.parameters` | [PostgreSQL parameters](https://www.postgresql.org/docs/current/config-setting.html#CONFIG-SETTING-CONFIGURATION-FILE)) | Some required and preferred settings |
|
||||
| `schedulerName` | Alternate scheduler name | `nil` |
|
||||
| `persistentVolume.accessModes` | Persistent Volume access modes | `[ReadWriteOnce]` |
|
||||
| `persistentVolume.annotations` | Annotations for Persistent Volume Claim` | `{}` |
|
||||
| `persistentVolume.mountPath` | Persistent Volume mount root path | `/var/lib/postgresql` |
|
||||
| `persistentVolume.size` | Persistent Volume size | `5Gi` |
|
||||
| `persistentVolume.storageClass` | Persistent Volume Storage Class | `volume.alpha.kubernetes.io/storage-class: default` |
|
||||
| `persistentVolume.subPath` | Subdirectory of Persistent Volume to mount | `""` |
|
||||
| `rbac.create` | Create required role and rolebindings | `true` |
|
||||
| `serviceAccount.create` | If true, create a new service account | `true` |
|
||||
| `serviceAccount.name` | Service account to be used. If not set and `serviceAccount.create` is `true`, a name is generated using the fullname template | `nil` |
|
||||
|
||||
### Examples
|
||||
- Override value using commandline parameters
|
||||
```console
|
||||
helm upgrade --install my-release . --set image.tag=pg12.5-ts2.0.0-p0 --set image.pullPolicy=Always
|
||||
```
|
||||
- Override values using `myvalues.yaml`
|
||||
```yaml
|
||||
# Filename: myvalues.yaml
|
||||
image:
|
||||
tag: pg12.5-ts2.0.0-p0
|
||||
pullPolicy: Always
|
||||
postgresql:
|
||||
databases:
|
||||
- postgres
|
||||
- proddb
|
||||
parameters:
|
||||
checkpoint_completion_target: 32MB
|
||||
work_mem: 16MB
|
||||
shared_buffers: 512MB
|
||||
```
|
||||
```console
|
||||
helm upgrade --install my-release . -f myvalues.yaml
|
||||
```
|
||||
|
||||
## Cleanup
|
||||
|
||||
Removing a deployment can be done by deleting a Helm deployment, however, removing the deployment does not remove:
|
||||
- the Persistent Volume Claims (pvc) belonging to the cluster
|
||||
|
||||
To fully purge a deployment in Kubernetes, you should do the following:
|
||||
```console
|
||||
# Delete the Helm deployment
|
||||
helm delete my-release
|
||||
# Delete pvc and the headless Patroni service
|
||||
kubectl delete $(kubectl get pvc -l release=my-release -o name)
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
### List Resources
|
||||
All the resources that are deployed can be listed by providing the filter `-l release=my-release`.
|
||||
|
||||
```console
|
||||
kubectl get all -l release=my-release
|
||||
```
|
||||
The output should be similar to the below output:
|
||||
```console
|
||||
NAME READY STATUS RESTARTS AGE
|
||||
pod/my-release-timescaledb-access-0 1/1 Running 0 11m
|
||||
pod/my-release-timescaledb-data-0 1/1 Running 0 11m
|
||||
pod/my-release-timescaledb-data-1 1/1 Running 0 11m
|
||||
pod/my-release-timescaledb-data-2 1/1 Running 0 11m
|
||||
|
||||
|
||||
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
|
||||
service/my-release-timescaledb LoadBalancer 10.152.183.60 <pending> 5432:31819/TCP 11m
|
||||
service/my-release-timescaledb-data ClusterIP None <none> 5432/TCP 11m
|
||||
|
||||
NAME READY AGE
|
||||
statefulset.apps/my-release-timescaledb-access 1/1 11m
|
||||
statefulset.apps/my-release-timescaledb-data 3/3 11m
|
||||
```
|
||||
|
||||
> **INFO** When listing resources within minutes of deploying a new Helm chart, you may see a list of jobs and its pods;
|
||||
these jobs are there to create the database, and to attach the data nodes to the access node. There will be quite a few,
|
||||
but these should disappear within minutes after successful deployment.
|
||||
|
||||
### Investigate TimescaleDB logs
|
||||
|
||||
The logs for the Access Node of TimescaleDB can be accessed as follows:
|
||||
|
||||
```console
|
||||
kubectl logs $(kubectl get pod -l release=my-release,timescaleNodeType=access) timescaledb
|
||||
```
|
||||
|
||||
### Verify multinode topology
|
||||
```console
|
||||
$ kubectl exec -ti $(kubectl get pod -l timescaleNodeType=access -o name) -c timescaledb -- psql -d example -c 'select node_name from timescaledb_information.data_nodes'
|
||||
```
|
||||
```text
|
||||
node_name
|
||||
-------------------------------
|
||||
my-release-timescaledb-data-0
|
||||
my-release-timescaledb-data-1
|
||||
my-release-timescaledb-data-2
|
||||
(3 rows)
|
||||
|
||||
```
|
||||
10
charts/ram-hostpath-sc.yaml
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
kind: StorageClass
|
||||
apiVersion: storage.k8s.io/v1
|
||||
metadata:
|
||||
name: ram-hostpath
|
||||
provisioner: microk8s.io/hostpath
|
||||
reclaimPolicy: Delete
|
||||
parameters:
|
||||
pvDir: /mnt/ramdisk
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
28
charts/templates/NOTES.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
TimescaleDB can be accessed via port 5432 on the following DNS name from within your cluster:
|
||||
{{ template "timescaledb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local
|
||||
|
||||
To get your password for superuser run:
|
||||
|
||||
# superuser password
|
||||
PGPASSWORD_SUPERUSER=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "timescaledb.fullname" . }} -o jsonpath="{.data.password-superuser}" | base64 --decode)
|
||||
|
||||
# admin password
|
||||
PGPASSWORD_ADMIN=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "timescaledb.fullname" . }} -o jsonpath="{.data.password-admin}" | base64 --decode)
|
||||
|
||||
To connect to your database:
|
||||
|
||||
1. Run a postgres pod and connect using the psql cli:
|
||||
# login as superuser
|
||||
kubectl run -i --tty --rm psql --image=postgres \
|
||||
--env "PGPASSWORD=$PGPASSWORD_SUPERUSER" \
|
||||
--command -- psql -U postgres \
|
||||
-h {{ template "timescaledb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local postgres
|
||||
|
||||
# login as admin
|
||||
kubectl run -i -tty --rm psql --image=postgres \
|
||||
--env "PGPASSWORD=$PGPASSWORD_ADMIN" \
|
||||
--command -- psql -U admin \
|
||||
-h {{ template "timescaledb.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local postgres
|
||||
60
charts/templates/_helpers.tpl
Normal file
@@ -0,0 +1,60 @@
|
||||
{{/*
|
||||
This file and its contents are licensed under the Apache License 2.0.
|
||||
Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
*/}}
|
||||
{{/* vim: set filetype=mustache: */}}
|
||||
{{/*
|
||||
Expand the name of the chart.
|
||||
*/}}
|
||||
{{- define "timescaledb.name" -}}
|
||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create a default fully qualified app name.
|
||||
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||
If release name contains chart name it will be used as a full name.
|
||||
*/}}
|
||||
{{- define "timescaledb.fullname" -}}
|
||||
{{- if .Values.fullnameOverride -}}
|
||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||
{{- if contains $name .Release.Name -}}
|
||||
{{- .Release.Name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- else -}}
|
||||
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
|
||||
{{- define "timescaledb.dataname" -}}
|
||||
{{ template "timescaledb.fullname" . }}-data
|
||||
{{- end -}}
|
||||
|
||||
|
||||
{{- define "timescaledb.accessname" -}}
|
||||
{{ template "timescaledb.fullname" . }}-access
|
||||
{{- end -}}
|
||||
|
||||
{{- define "postgres.uid" -}}
|
||||
{{- default .Values.uid "1000" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create chart name and version as used by the chart label.
|
||||
*/}}
|
||||
{{- define "timescaledb.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}}
|
||||
{{- end -}}
|
||||
|
||||
{{/*
|
||||
Create the name of the service account to use.
|
||||
*/}}
|
||||
{{- define "timescaledb.serviceAccountName" -}}
|
||||
{{- if .Values.serviceAccount.create -}}
|
||||
{{ default (include "timescaledb.fullname" .) .Values.serviceAccount.name }}
|
||||
{{- else -}}
|
||||
{{ default "default" .Values.serviceAccount.name }}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
77
charts/templates/job-attach-datanode.yaml
Normal file
@@ -0,0 +1,77 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
{{- range $pod, $e := until ( .Values.dataNodes | int) }}
|
||||
{{- range $index, $dbname := $.Values.postgresql.databases }}
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ printf "attachdn-%s-db%s-data%s" $.Release.Name ($index | toString) ($pod | toString) | trunc 63 }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" $ }}
|
||||
chart: {{ template "timescaledb.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
annotations:
|
||||
"helm.sh/hook-delete-policy": hook-succeeded
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" $ }}
|
||||
chart: {{ template "timescaledb.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
dataNode: {{ template "timescaledb.dataname" $ }}-{{ $pod }}
|
||||
spec:
|
||||
containers:
|
||||
- name: attachdn-{{ $index }}
|
||||
image: postgres:14.5-alpine # A relatively small official image that can run psql
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
# We wait for the data node to allow connections
|
||||
# We wait for the access node to allow connections to DBNAME
|
||||
- >
|
||||
while ! pg_isready -U postgres -h "${DATA_NODE_DNS}"; do sleep 1; done;
|
||||
while ! psql -d "${ACCESS_SVC_CONNSTR}" --set dbname="${DBNAME}" --set ON_ERROR_STOP=1 --command '\c :"dbname"'; do sleep 1; done;
|
||||
echo "${SQLCOMMAND}" | psql -d "${ACCESS_SVC_CONNSTR}" --file=- --echo-queries --set ON_ERROR_STOP=1 \
|
||||
--set dbname="${DBNAME}" \
|
||||
--set data_node_name="${DATA_NODE_NAME}" \
|
||||
--set data_node_dns="${DATA_NODE_DNS}"
|
||||
env:
|
||||
{{- /*
|
||||
Some parameter juggling is required to ensure we don't have SQL injection;
|
||||
which is not necessarily a major security leak at this stage, but we want
|
||||
to be able to support database names like 'test db' or, 'CamelCase'.
|
||||
|
||||
The template quote function ensures bash will be able to interpret the variable.
|
||||
The --set dbname= and subsequent :'dbname' psql_variable ensures no SQL injection can occur.
|
||||
|
||||
https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-INTERPOLATION
|
||||
*/}}
|
||||
- name: DBNAME
|
||||
value: {{ $dbname | quote }}
|
||||
- name: ACCESS_SVC_CONNSTR
|
||||
value: host={{ template "timescaledb.fullname" $ }} user=postgres connect_timeout=3 sslmode=disable
|
||||
- name: DATA_NODE_DNS
|
||||
value: {{ template "timescaledb.dataname" $ }}-{{ $pod }}.{{ template "timescaledb.dataname" $ }}
|
||||
- name: DATA_NODE_NAME
|
||||
value: {{ template "timescaledb.dataname" $ }}-{{ $pod }}
|
||||
- name: SQLCOMMAND
|
||||
value: |
|
||||
\c :"dbname"
|
||||
SELECT *
|
||||
FROM add_data_node(:'data_node_name'::name, host => :'data_node_dns', if_not_exists => true)
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "timescaledb.accessname" $ }}
|
||||
key: password-superuser
|
||||
restartPolicy: OnFailure
|
||||
backoffLimit: 2
|
||||
...
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
72
charts/templates/job-create-databases.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
{{- range $index, $dbname := .Values.postgresql.databases }}
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ printf "createdb-%s-db%s" $.Release.Name ($index | toString) | trunc 63 }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" $ }}
|
||||
chart: {{ template "timescaledb.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
annotations:
|
||||
"helm.sh/hook-delete-policy": hook-succeeded
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 600
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" $ }}
|
||||
chart: {{ template "timescaledb.chart" $ }}
|
||||
release: {{ $.Release.Name }}
|
||||
heritage: {{ $.Release.Service }}
|
||||
spec:
|
||||
containers:
|
||||
- name: createdb-{{ $index }}
|
||||
image: postgres:14.5-alpine # A relatively small official image that can run psql
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- >
|
||||
while ! pg_isready -U postgres -h {{ template "timescaledb.fullname" $ }}; do sleep 1; done;
|
||||
echo "${SQLCOMMAND}" | psql --file=- --echo-queries -d "${ACCESS_SVC_CONNSTR}" \
|
||||
--set ON_ERROR_STOP=1 \
|
||||
--set dbname="${DBNAME}"
|
||||
env:
|
||||
{{- /*
|
||||
Some parameter juggling is required to ensure we don't have SQL injection;
|
||||
which is not necessarily a major security leak at this stage, but we want
|
||||
to be able to support database names like 'test db' or, 'CamelCase'.
|
||||
|
||||
The template quote function ensures bash will be able to interpret the variable.
|
||||
The --set dbname= and subsequent :'dbname' psql_variable ensures no SQL injection can occur.
|
||||
|
||||
https://www.postgresql.org/docs/current/app-psql.html#APP-PSQL-INTERPOLATION
|
||||
*/}}
|
||||
- name: DBNAME
|
||||
value: {{ $dbname | quote }}
|
||||
- name: ACCESS_SVC_CONNSTR
|
||||
value: host={{ template "timescaledb.fullname" $ }} user=postgres connect_timeout=3 sslmode=disable
|
||||
- name: SQLCOMMAND
|
||||
value: |
|
||||
SELECT format('CREATE DATABASE %I', :'dbname')
|
||||
WHERE NOT EXISTS (
|
||||
SELECT
|
||||
FROM pg_database
|
||||
WHERE datname=:'dbname'
|
||||
)
|
||||
\gexec
|
||||
\c :"dbname"
|
||||
CREATE EXTENSION IF NOT EXISTS timescaledb;
|
||||
- name: PGPASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "timescaledb.accessname" $ }}
|
||||
key: password-superuser
|
||||
restartPolicy: OnFailure
|
||||
backoffLimit: 2
|
||||
...
|
||||
{{ end }}
|
||||
31
charts/templates/sec-timescaledb.yaml
Normal file
@@ -0,0 +1,31 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "timescaledb.accessname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
type: Opaque
|
||||
data:
|
||||
password-superuser: {{ .Values.credentials.accessNode.superuser | b64enc }}
|
||||
...
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
type: Opaque
|
||||
data:
|
||||
password-superuser: {{ .Values.credentials.dataNode.superuser | b64enc }}
|
||||
...
|
||||
14
charts/templates/serviceaccount-timescaledb.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
{{- if .Values.serviceAccount.create }}
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: {{ template "timescaledb.serviceAccountName" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
166
charts/templates/statefulset-timescaledb-accessnode.yaml
Normal file
@@ -0,0 +1,166 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ template "timescaledb.accessname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
serviceName: {{ template "timescaledb.accessname" . }}
|
||||
replicas: 1
|
||||
podManagementPolicy: Parallel
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
timescaleNodeType: access
|
||||
template:
|
||||
metadata:
|
||||
name: {{ template "timescaledb.accessname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
timescaleNodeType: access
|
||||
spec:
|
||||
serviceAccountName: {{ template "timescaledb.serviceAccountName" . }}
|
||||
securityContext:
|
||||
# The postgres user inside the TimescaleDB image has uid=1000.
|
||||
# This configuration ensures the permissions of the mounts are suitable
|
||||
fsGroup: {{ template "postgres.uid" }}
|
||||
runAsGroup: {{ template "postgres.uid" }}
|
||||
runAsNonRoot: true
|
||||
runAsUser: {{ template "postgres.uid" }}
|
||||
initContainers:
|
||||
- name: initdb
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: POSTGRESQL_CUSTOM_PARAMETERS
|
||||
value: |
|
||||
{{- range $key, $value := .Values.postgresql.parameters }}
|
||||
{{ printf "%s = '%s'" $key ($value | toString) }}
|
||||
{{- end }}
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "timescaledb.accessname" . }}
|
||||
key: password-superuser
|
||||
- name: POSTGRES_PASSWORD_DATA_NODE
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
key: password-superuser
|
||||
{{- if .Values.env }}
|
||||
{{ .Values.env | default list | toYaml | indent 8 }}
|
||||
{{- end }}
|
||||
command:
|
||||
- sh
|
||||
- '-c'
|
||||
# By calling the original entrypoint with the first argument being postgres
|
||||
# we ensure we do everything that is required to init a PostgreSQL instance.
|
||||
# By supplying --single however, we ensure the postmaster is running in the
|
||||
# foreground, allowing us to do some more initialization
|
||||
- |
|
||||
set -e
|
||||
install -o postgres -g postgres -m 0700 -d "${PGDATA}"
|
||||
/docker-entrypoint.sh postgres --single < /dev/null
|
||||
grep -qxF "include 'postgresql_helm_customizations.conf'" "${PGDATA}/postgresql.conf" \
|
||||
|| echo "include 'postgresql_helm_customizations.conf'" >> "${PGDATA}/postgresql.conf"
|
||||
echo "Writing custom PostgreSQL Parameters to ${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "cluster_name = '$(hostname)'" > "${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "${POSTGRESQL_CUSTOM_PARAMETERS}" | sort >> "${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "*:*:*:postgres:${POSTGRES_PASSWORD_DATA_NODE}" > "${PGDATA}/../.pgpass"
|
||||
chown postgres:postgres "${PGDATA}/../.pgpass" "${PGDATA}/postgresql_helm_customizations.conf"
|
||||
chmod 0600 "${PGDATA}/../.pgpass"
|
||||
echo "Adding host all all all md5 in pg_hba.conf"
|
||||
grep -qxF "host all all all md5" "${PGDATA}/pg_hba.conf" \
|
||||
|| echo "host all all all md5" >> ${PGDATA}/pg_hba.conf
|
||||
volumeMounts:
|
||||
- name: storage-volume
|
||||
mountPath: "{{ .Values.persistentVolume.mountPath }}"
|
||||
subPath: "{{ .Values.persistentVolume.subPath }}"
|
||||
containers:
|
||||
- name: timescaledb
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# We start postgres with a fully cleared environment
|
||||
command:
|
||||
- sh
|
||||
- '-c'
|
||||
- exec env -i PGDATA="${PGDATA}" PATH="${PATH}" /docker-entrypoint.sh postgres
|
||||
env:
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
{{- if .Values.env }}
|
||||
{{ .Values.env | default list | toYaml | indent 8 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: storage-volume
|
||||
mountPath: "{{ .Values.persistentVolume.mountPath }}"
|
||||
subPath: "{{ .Values.persistentVolume.subPath }}"
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 10 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.schedulerName }}
|
||||
schedulerName: {{ .Values.schedulerName }}
|
||||
{{- end }}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- an
|
||||
{{- if not .Values.persistentVolume.enabled }}
|
||||
- name: storage-volume
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.persistentVolume.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: storage-volume
|
||||
annotations:
|
||||
{{- if .Values.persistentVolume.annotations }}
|
||||
{{ toYaml .Values.persistentVolume.annotations | indent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{ toYaml .Values.persistentVolume.accessModes | indent 8 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .Values.persistentVolume.size }}"
|
||||
{{- if .Values.persistentVolume.storageClass }}
|
||||
{{- if (eq "-" .Values.persistentVolume.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.persistentVolume.storageClass }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
161
charts/templates/statefulset-timescaledb-datanode.yaml
Normal file
@@ -0,0 +1,161 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
serviceName: {{ template "timescaledb.dataname" . }}
|
||||
replicas: {{ .Values.dataNodes }}
|
||||
podManagementPolicy: Parallel
|
||||
selector:
|
||||
matchLabels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
timescaleNodeType: data
|
||||
template:
|
||||
metadata:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
timescaleNodeType: data
|
||||
spec:
|
||||
serviceAccountName: {{ template "timescaledb.serviceAccountName" . }}
|
||||
securityContext:
|
||||
# The postgres user inside the TimescaleDB image has uid=1000.
|
||||
# This configuration ensures the permissions of the mounts are suitable
|
||||
fsGroup: {{ template "postgres.uid" }}
|
||||
runAsGroup: {{ template "postgres.uid" }}
|
||||
runAsNonRoot: true
|
||||
runAsUser: {{ template "postgres.uid" }}
|
||||
initContainers:
|
||||
- name: initdb
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
env:
|
||||
- name: POSTGRESQL_CUSTOM_PARAMETERS
|
||||
value: |
|
||||
{{- range $key, $value := .Values.postgresql.parameters }}
|
||||
{{ printf "%s = '%s'" $key ($value | toString) }}
|
||||
{{- end }}
|
||||
- name: POSTGRES_PASSWORD
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
key: password-superuser
|
||||
{{- if .Values.env }}
|
||||
{{ .Values.env | default list | toYaml | indent 8 }}
|
||||
{{- end }}
|
||||
command:
|
||||
- sh
|
||||
- '-c'
|
||||
# By calling the original entrypoint with the first argument being postgres
|
||||
# we ensure we do everything that is required to init a PostgreSQL instance.
|
||||
# By supplying --single however, we ensure the postmaster is running in the
|
||||
# foreground, allowing us to do some more initialization
|
||||
- |
|
||||
set -e
|
||||
install -o postgres -g postgres -m 0700 -d "${PGDATA}" "${PGDATA}/../conf.d"
|
||||
/docker-entrypoint.sh postgres --single < /dev/null
|
||||
grep -qxF "include 'postgresql_helm_customizations.conf'" "${PGDATA}/postgresql.conf" \
|
||||
|| echo "include 'postgresql_helm_customizations.conf'" >> "${PGDATA}/postgresql.conf"
|
||||
echo "Writing custom PostgreSQL Parameters to ${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "cluster_name = '$(hostname)'" > "${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "${POSTGRESQL_CUSTOM_PARAMETERS}" | sort >> "${PGDATA}/postgresql_helm_customizations.conf"
|
||||
echo "Adding host all all all md5 in pg_hba.conf"
|
||||
grep -qxF "host all all all md5" "${PGDATA}/pg_hba.conf" \
|
||||
|| echo "host all all all md5" >> ${PGDATA}/pg_hba.conf
|
||||
# The TimescaleDB extension should not be available by default, as this interferes with the bootstrapping
|
||||
# done by the access nodes. Therefore we drop the extensions from template1
|
||||
echo "DROP EXTENSION timescaledb" | /docker-entrypoint.sh postgres --single -D "${PGDATA}" template1
|
||||
volumeMounts:
|
||||
- name: storage-volume
|
||||
mountPath: "{{ .Values.persistentVolume.mountPath }}"
|
||||
subPath: "{{ .Values.persistentVolume.subPath }}"
|
||||
containers:
|
||||
- name: timescaledb
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||
# We start postgres with a fully cleared environment
|
||||
command:
|
||||
- sh
|
||||
- '-c'
|
||||
- exec env -i PGDATA="${PGDATA}" PATH="${PATH}" /docker-entrypoint.sh postgres
|
||||
env:
|
||||
- name: POD_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
apiVersion: v1
|
||||
fieldPath: metadata.namespace
|
||||
{{- if .Values.env }}
|
||||
{{ .Values.env | default list | toYaml | indent 8 }}
|
||||
{{- end }}
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
volumeMounts:
|
||||
- name: storage-volume
|
||||
mountPath: "{{ .Values.persistentVolume.mountPath }}"
|
||||
subPath: "{{ .Values.persistentVolume.subPath }}"
|
||||
resources:
|
||||
{{ toYaml .Values.resources | indent 10 }}
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- with .Values.tolerations }}
|
||||
tolerations:
|
||||
{{ toYaml . | indent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.schedulerName }}
|
||||
schedulerName: {{ .Values.schedulerName }}
|
||||
{{- end }}
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: NotIn
|
||||
values:
|
||||
- an
|
||||
{{- if not .Values.persistentVolume.enabled }}
|
||||
- name: storage-volume
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- if .Values.persistentVolume.enabled }}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: storage-volume
|
||||
annotations:
|
||||
{{- if .Values.persistentVolume.annotations }}
|
||||
{{ toYaml .Values.persistentVolume.annotations | indent 8 }}
|
||||
{{- end }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
accessModes:
|
||||
{{ toYaml .Values.persistentVolume.accessModes | indent 8 }}
|
||||
resources:
|
||||
requests:
|
||||
storage: "{{ .Values.persistentVolume.size }}"
|
||||
{{- if .Values.persistentVolume.storageClass }}
|
||||
{{- if (eq "-" .Values.persistentVolume.storageClass) }}
|
||||
storageClassName: ""
|
||||
{{- else }}
|
||||
storageClassName: "{{ .Values.persistentVolume.storageClass }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
25
charts/templates/svc-timescaledb-access.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ template "timescaledb.fullname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
annotations:
|
||||
service.beta.kubernetes.io/aws-load-balancer-connection-idle-timeout: "4000"
|
||||
spec:
|
||||
type: LoadBalancer
|
||||
ports:
|
||||
- name: postgresql
|
||||
port: 5432
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
timescaleNodeType: access
|
||||
...
|
||||
23
charts/templates/svc-timescaledb-data.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: {{ template "timescaledb.dataname" . }}
|
||||
labels:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
chart: {{ template "timescaledb.chart" . }}
|
||||
release: {{ .Release.Name }}
|
||||
heritage: {{ .Release.Service }}
|
||||
spec:
|
||||
clusterIP: None
|
||||
ports:
|
||||
- name: postgresql
|
||||
port: 5432
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: {{ template "timescaledb.fullname" . }}
|
||||
timescaleNodeType: data
|
||||
...
|
||||
130
charts/values.yaml
Normal file
@@ -0,0 +1,130 @@
|
||||
# This file and its contents are licensed under the Apache License 2.0.
|
||||
# Please see the included NOTICE for copyright information and LICENSE for a copy of the license.
|
||||
|
||||
dataNodes: 3
|
||||
|
||||
# To prevent very long names, we override the name, otherwise it would default to
|
||||
# timescaledb-multinode (the name of the chart)
|
||||
nameOverride: timescaledb
|
||||
|
||||
image:
|
||||
# Image was built from
|
||||
# https://github.com/timescale/timescaledb-docker-ha
|
||||
repository: timescale/timescaledb-ha
|
||||
tag: pg14.5-ts2.8.0-patroni-static-primary-latest
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
# Credentials used by PostgreSQL
|
||||
credentials:
|
||||
accessNode:
|
||||
superuser: big_dick
|
||||
dataNode:
|
||||
superuser: big_dick
|
||||
|
||||
# Extra custom environment variables.
|
||||
# These should be an EnvVar, as this allows you to inject secrets into the environment
|
||||
# https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.16/#envvar-v1-core
|
||||
env:
|
||||
- name: LC_ALL
|
||||
value: C.UTF-8
|
||||
- name: LANG
|
||||
value: C.UTF-8
|
||||
- name: PGDATA
|
||||
# This should be a subdirectory of the persistentVolume (if any), as PostgreSQL will need to
|
||||
# fully manage permissions. Also, using /var/lib/postgresql/data is discouraged, as this is
|
||||
# a Docker Volume in many Docker images, which means the data is not actually persisted.
|
||||
value: /var/lib/postgresql/pgdata
|
||||
|
||||
persistentVolume:
|
||||
enabled: true
|
||||
size: 100G
|
||||
## database data Persistent Volume Storage Class
|
||||
## If defined, storageClassName: <storageClass>
|
||||
## If set to "-", storageClassName: "", which disables dynamic provisioning
|
||||
## If undefined (the default) or set to null, no storageClassName spec is
|
||||
## set, choosing the default provisioner. (gp2 on AWS, standard on
|
||||
## GKE, AWS & OpenStack)
|
||||
##
|
||||
# storageClass: "-"
|
||||
subPath: ""
|
||||
mountPath: "/var/lib/postgresql"
|
||||
annotations: {}
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
|
||||
resources: {}
|
||||
|
||||
postgresql:
|
||||
databases:
|
||||
- postgres
|
||||
- scada
|
||||
parameters:
|
||||
max_connections: 300
|
||||
max_prepared_transactions: 300
|
||||
effective_cache_size: 96272MB
|
||||
maintenance_work_mem: 2047MB
|
||||
max_worker_processes: 367
|
||||
max_parallel_workers_per_gather: 32
|
||||
max_parallel_workers: 64
|
||||
wal_buffers: 16MB
|
||||
shared_buffers: 32090MB
|
||||
work_mem: 5134kB
|
||||
default_statistics_target: 500
|
||||
random_page_cost: 1.1
|
||||
checkpoint_completion_target: 0.9
|
||||
max_locks_per_transaction: 512
|
||||
autovacuum_max_workers: 10
|
||||
autovacuum_naptime: 10
|
||||
effective_io_concurrency: 256
|
||||
log_connections: 'on'
|
||||
log_line_prefix: "%t [%p]: [%c-%l] %u@%d,app=%a [%e] "
|
||||
log_min_duration_statement: '1s'
|
||||
log_statement: ddl
|
||||
log_checkpoints: 'on'
|
||||
log_lock_waits: 'on'
|
||||
max_wal_senders: 0
|
||||
wal_level: minimal
|
||||
fsync: false
|
||||
min_wal_size: 512MB
|
||||
wal_writer_delay: 1000
|
||||
temp_file_limit: 1GB
|
||||
timescaledb.passfile: '/var/lib/postgresql/.pgpass'
|
||||
synchronous_commit: 'off'
|
||||
timescaledb.enable_2pc: false
|
||||
timescaledb.max_background_workers: 300
|
||||
timezone: 'Asia/Shanghai'
|
||||
|
||||
# https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#nodeselector
|
||||
nodeSelector: {}
|
||||
|
||||
# https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/
|
||||
tolerations: []
|
||||
|
||||
# https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity
|
||||
affinityTemplate: |
|
||||
podAntiAffinity:
|
||||
preferredDuringSchedulingIgnoredDuringExecution:
|
||||
- weight: 100
|
||||
podAffinityTerm:
|
||||
topologyKey: "kubernetes.io/hostname"
|
||||
labelSelector:
|
||||
matchLabels:
|
||||
app: {{ template "timescaledb.name" . }}
|
||||
release: {{ .Release.Name | quote }}
|
||||
affinity: {}
|
||||
|
||||
## Use an alternate scheduler, e.g. "stork".
|
||||
## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/
|
||||
##
|
||||
# schedulerName:
|
||||
|
||||
rbac:
|
||||
# Specifies whether RBAC resources should be created
|
||||
create: true
|
||||
|
||||
serviceAccount:
|
||||
# Specifies whether a ServiceAccount should be created
|
||||
create: true
|
||||
# The name of the ServiceAccount to use.
|
||||
# If not set and create is true, a name is generated using the fullname template
|
||||
name:
|
||||
@@ -1,5 +1,5 @@
|
||||
services:
|
||||
scada:
|
||||
edge_manager:
|
||||
build:
|
||||
context: .
|
||||
container_name: edge_manager
|
||||
|
||||
6
docs/book.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
[book]
|
||||
authors = ["Yu Sun"]
|
||||
language = "en"
|
||||
multilingual = false
|
||||
src = "src"
|
||||
title = "EdgeManager开发指北"
|
||||
26
docs/src/SUMMARY.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# Summary
|
||||
|
||||
[项目简介](introduction.md)
|
||||
|
||||
[系统配置](system.md)
|
||||
|
||||
|
||||
# SCADA系统
|
||||
|
||||
- [环境搭建](scada/environment.md)
|
||||
- [作为MES插件使用](scada/plug_in.md)
|
||||
- [前端配置](scada/front.md)
|
||||
- [后端配置](scada/service.md)
|
||||
- [API](scada/api.md)
|
||||
- [技术细节](scada/details.md)
|
||||
|
||||
|
||||
# 测试
|
||||
|
||||
- [集群搭建](tests/cluster.md)
|
||||
- [运行多节点TimescaleDB服务](tests/tsdb.md)
|
||||
- [技术细节](tests/details.md)
|
||||
- [常见问题](tests/questions/questions.md)
|
||||
- [Pods无法清除](tests/questions/terminating.md)
|
||||
- [Pods创建失败](tests/questions/failed_pod.md)
|
||||
- [使用物理内存作持久化存储](tests/questions/ram.md)
|
||||
3
docs/src/introduction.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 项目简介
|
||||
|
||||
待补充,请阅读其他内容。
|
||||
91
docs/src/scada/api.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# API
|
||||
> API设计最大程度遵循了已有的MES规范。
|
||||
|
||||
## 请求说明
|
||||
>上位机程序中请单次传入尽量多的数据使得性能最大化。
|
||||
>
|
||||
>但仍需兼顾传输速率和超时时间。
|
||||
>
|
||||
>无需指定数值类型,服务端会自动根据已添加的节点信息检查,若不符则会报错。
|
||||
|
||||
### set_node_data,上传设备实时数据
|
||||
#### 输入接口参数说明:
|
||||
|
||||
| 参数名称 | 参数说明 | 类型 | 是否必传 |备注 |
|
||||
| ---- | ---- | ---- | ---- |---- |
|
||||
| action | 接口名称 |String | 是 |
|
||||
| param | 接口参数 |Object | 是|
|
||||
| working_subclass | 工序单元属性 |String | 可选 | 通过数据系统对各工序动态定义|
|
||||
| data | 上传数值 |Array | 是|
|
||||
| code | 值 | String | 是 |
|
||||
| value | 值 |string/int/float/bool | 是 | value的类型需与数据库节点配置表的type对应|
|
||||
| device_code | 设备编码 |string | 是 |
|
||||
| parent_device_code | 值 | String | 可选 | MES系统中的设备编号,上传设备状态时为必传|
|
||||
| batch | 批次号 | String | 可选 | MES系统中的生产批次号|
|
||||
|
||||
```json
|
||||
{
|
||||
"action": "set_node_data",
|
||||
"param": {
|
||||
"working_subclass": <string>, // 可选
|
||||
"data": [
|
||||
{
|
||||
"code": <string>,
|
||||
"value": <string | int | float | bool>,// value的类型需与type对应
|
||||
"device_code": [string], // 必需字段
|
||||
"parent_device_code": [string], // 可选字段,对应MES中的device_code
|
||||
"batch": [string] // 可选字段
|
||||
},
|
||||
{
|
||||
//code的值在后台的数据类别为设备状态类型时
|
||||
"code": <string>,
|
||||
// value的类型需与type对应
|
||||
"value": <string | int >,
|
||||
"device_code": [string], // 必需字段
|
||||
"parent_device_code": [string], // 必需字段,对应MES中的code
|
||||
"batch": [string] // 可选字段
|
||||
},
|
||||
{
|
||||
"code": <string>,
|
||||
"value": <string | int | float | bool>,
|
||||
"device_code": [string],
|
||||
"parent_device_code": [string],
|
||||
"batch": [string]
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 输出接口参数说明:
|
||||
|
||||
| 参数名称 | 参数说明 | 类型 | 长度 |备注 |
|
||||
| ----- | ----- | ----- | ----- |----- |
|
||||
| action | 接口名称 |String | 45 |
|
||||
| errcode | 返回码 |Int | 11 |0:请求成功<br>4002:未登记过的工序单元!<br>4002:节点编码和数值类型不匹配!<br>4002:ROLLBACKed: Bad data received (structure and/or values) |
|
||||
| errmsg | 错误信息 |String | 255 |errcode 不为0,errmsg才有错误信息 |
|
||||
|
||||
```
|
||||
{
|
||||
action: "result_set_node_data",
|
||||
errcode: 0,
|
||||
errmsg: ""
|
||||
}
|
||||
```
|
||||
|
||||
## 上传设备状态
|
||||
>数据节点的数据类型在后台设定为设备状态时,该节点上传的值只能上传int或string两种类型。
|
||||
>
|
||||
>并且int或string这两种类型,只能上传下方表格中int或string指定的值,若不符则会报错。
|
||||
>
|
||||
>上传对应的设备状态parent_device_code为必传字段
|
||||
|
||||
| int类型 | string类型 | 描述 |
|
||||
| ----- | ----- | ----- |
|
||||
| 1 | IDLE | 设备已经初始化,再等待工作 |
|
||||
| 2 | RUN | 设备在工作 |
|
||||
| 3 | FINISH | 工序结束后,托盘还没取出时,设备为FINISH,取出后变为IDLE状态 |
|
||||
| 4 | TROUBLE | 设备有问题,需要维修,这时msg有相应的关键字 |
|
||||
| 5 | PAUSE | 设备通过手工操作变成暂停状态 |
|
||||
| 6 | OFFLINE | 设备处于离线状态 |
|
||||
39
docs/src/scada/details.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## 技术细节
|
||||
|
||||
### 0. `EdgeManager\EDataCapture\EDataCapture -> set_data()`为什么是以`6710885`为大小chunked的?
|
||||
|
||||
首先明确一点,根据PostgreSQL的技术架构,条件允许的情况下,**一次性插入多条记录是效率最高的**,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films (code, title, did, date_prod, kind) VALUES
|
||||
('B6717', 'Tampopo', 110, '1985-02-10', 'Comedy'),
|
||||
('HG120', 'The Dinner Game', 140, DEFAULT, 'Comedy');
|
||||
```
|
||||
|
||||
这其中,仅插入部分字段的值,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films (code, title, did, date_prod, kind)
|
||||
VALUES ('T_601', 'Yojimbo', 106, '1961-06-16', 'Drama');
|
||||
```
|
||||
|
||||
的效率又远远不如插入全部列,但使用特殊变量DEFAULT替代缺少的字段,比如:
|
||||
|
||||
```sql
|
||||
INSERT INTO films VALUES
|
||||
('UA502', 'Bananas', DEFAULT, '1971-07-13', 'Comedy', '82 minutes');
|
||||
```
|
||||
|
||||
PostgreSQL的文档里没有提及这个话题,因为源码里为SQL语句的内存分配[指定了一个最大长度常量`MaxAllocSize`](https://github.com/postgres/postgres/blob/2373fe78dfc9d4aa2348a86fffdf8eb9d757e9d5/src/common/stringinfo.c#L28),其大小为比1GB小1字节。这个值可以在编译前手动修改,所以理论上每个PG运行的实例都可以不一样,如果我们使用的PG并非修改过源码手动编译的,那么SQL语句的最大长度为`1024 ** 3 - 1 = 1073741823` bytes。
|
||||
|
||||
> 安全起见,下述计算假定working_subclass等字段的最大长度均为我们以往约定的45 bytes。
|
||||
|
||||
而此处我源码中的SQL语句头长度为138 bytes(注意末尾空格):
|
||||
|
||||

|
||||
|
||||
单个语句体的长度为160 bytes:
|
||||
|
||||

|
||||
|
||||
所以其最多可以一次性插入`(1073741823 - 138) / 160`条记录,向下取整后即为`6710885`。
|
||||
77
docs/src/scada/environment.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# 环境搭建
|
||||
## 开发环境
|
||||
|
||||
拉取代码:
|
||||
|
||||
```bash
|
||||
# 建议先配置SSH key pair
|
||||
git clone ssh://git@118.195.187.246:10022/ysun/EdgeManager.git
|
||||
cd EdgeManager
|
||||
```
|
||||
|
||||
一键部署PHP workerman和TimescaleDB环境:
|
||||
|
||||
```bash
|
||||
# docker build --network host -t edge_manager .
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
进入交互式Prompt:
|
||||
|
||||
```bash
|
||||
docker exec -it edge_manager sh
|
||||
```
|
||||
|
||||
后端调试:
|
||||
|
||||
>--no_dup_code:禁止code在不同的working subclass间复用
|
||||
>
|
||||
>--relay_device_status:不判断是否是设备状态并转发到MES接口
|
||||
>
|
||||
>--port:连接PG数据库端口,默认5432端口
|
||||
|
||||
```bash
|
||||
# In container
|
||||
php EdgeManager.php --no_dup_code --relay_device_status --server_name=GPU-server-01 --user=postgres --password=big_dick start
|
||||
```
|
||||
|
||||
前端调试:
|
||||
|
||||
```bash
|
||||
# In host
|
||||
# yarn
|
||||
# yarn watch
|
||||
yarn serve
|
||||
```
|
||||
|
||||
客户端PG连接:
|
||||
|
||||
```bash
|
||||
# sudo apt install postgresql-client
|
||||
# 登入
|
||||
psql -h localhost -U postgres
|
||||
# 显示数据库列表
|
||||
\l
|
||||
```
|
||||
|
||||
## 调试环境
|
||||
|
||||
不使用`docker compose`创建两个container分别运行EdgeManager和pg:
|
||||
|
||||
```bash
|
||||
# cd EdgeManager # 先定位到项目目录,方便创建image和挂载
|
||||
# 创建EdgeManager的image
|
||||
docker build --network host -t edge_manager .
|
||||
# 创建container运行EdgeManager
|
||||
docker run -d --name edge_manager_test -v $PWD:/EdgeManager --network host --ipc host -it edge_manager
|
||||
# 创建container运行pg,将端口映射到host的55432
|
||||
docker run -d --name pg_test -v $PWD/config/postgresql.conf:/etc/postgresql/postgresql.conf -p 55432:5432 -e POSTGRES_PASSWORD=big_dick -it timescale/timescaledb-ha:pg14-latest postgres -c 'config_file=/etc/postgresql/postgresql.conf'
|
||||
# 进入交互式Prompt
|
||||
docker exec -it edge_manager_test sh
|
||||
# 启动EdgeManager(workerman)命令省略...
|
||||
|
||||
# 常用命令
|
||||
# 查看全部container
|
||||
docker ps -a
|
||||
# 启动已停止的container
|
||||
docker start [container]
|
||||
91
docs/src/scada/front.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# 前端配置
|
||||
添加依赖(**使用npm**):
|
||||
|
||||
```bash
|
||||
npm i element-ui \
|
||||
@d2-projects/d2-crud \
|
||||
vue-cheetah-grid \
|
||||
@d2-projects/vue-table-export \
|
||||
@d2-projects/vue-table-import \
|
||||
github-markdown-css \
|
||||
marked@^2.0.0 \
|
||||
jschardet -S
|
||||
```
|
||||
|
||||
添加依赖(**使用yarn**):
|
||||
```bash
|
||||
yarn add element-ui \
|
||||
@d2-projects/d2-crud \
|
||||
vue-cheetah-grid \
|
||||
@d2-projects/vue-table-export \
|
||||
@d2-projects/vue-table-import \
|
||||
github-markdown-css \
|
||||
marked@^2.0.0 \
|
||||
jschardet
|
||||
```
|
||||
|
||||
需要在`main.js`中增加(全局引入组件):
|
||||
|
||||
```js
|
||||
// D2-Crud
|
||||
import ElementUI from 'element-ui'
|
||||
import 'element-ui/lib/theme-chalk/index.css'
|
||||
import D2Crud from '@d2-projects/d2-crud'
|
||||
// Cheetah-Grid
|
||||
import vueCheetahGrid from 'vue-cheetah-grid'
|
||||
// 表格导出插件
|
||||
import pluginExport from '@d2-projects/vue-table-export'
|
||||
import pluginImport from '@d2-projects/vue-table-import'
|
||||
|
||||
Vue.use(ElementUI)
|
||||
Vue.use(D2Crud)
|
||||
Vue.use(vueCheetahGrid)
|
||||
Vue.use(pluginExport)
|
||||
Vue.use(pluginImport)
|
||||
```
|
||||
|
||||
*(可选)* 在`package.json`里更改`element-ui`版本:
|
||||
|
||||
```json
|
||||
"element-ui": ">2.15.9 || 2.15.8",
|
||||
```
|
||||
|
||||
`2.15.9`有一个小[bug](https://github.com/ElemeFE/element/issues/21941),会导致性能下降。
|
||||
|
||||
此外请参照目录结构中注释进行代码合并:
|
||||
|
||||
```pre
|
||||
📦src
|
||||
┣ 📂api
|
||||
┃ ┣ 📂modules
|
||||
┃ ┃ ┣ 📜scada.configure.api.js # 增添Axios请求
|
||||
┃ ┃ ┗ 📜sys.user.api.js
|
||||
┃ ┣ 📜index.js
|
||||
┃ ┣ 📜service.js
|
||||
┃ ┗ 📜tools.js
|
||||
┣ 📂assets
|
||||
┣ 📂components
|
||||
┃ ┣ 📂d2-markdown # 渲染markdown所需组件(在D2Admin的基础上精简了功能)
|
||||
┃ ┃ ┗ 📜index.vue
|
||||
┃ ┗ 📜index.js # 在此处注册d2-markdown
|
||||
┣ 📂libs
|
||||
┣ 📂locales
|
||||
┣ 📂menu
|
||||
┃ ┗ 📜index.js # 增添菜单
|
||||
┣ 📂plugin
|
||||
┣ 📂router
|
||||
┃ ┣ 📜index.js
|
||||
┃ ┗ 📜routes.js # 增添路由
|
||||
┣ 📂store
|
||||
┣ 📂views
|
||||
┃ ┣ 📂scada # 增添页面
|
||||
┃ ┃ ┣ 📂scadaConfigure
|
||||
┃ ┃ ┃ ┗ 📜index.vue
|
||||
┃ ┃ ┗ 📂scadaQuery
|
||||
┃ ┃ ┃ ┗ 📜index.vue
|
||||
┃ ┗ 📂system
|
||||
┣ 📜App.vue
|
||||
┣ 📜i18n.js
|
||||
┣ 📜main.js
|
||||
┗ 📜setting.js
|
||||
```
|
||||
6
docs/src/scada/plug_in.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# SCADA系统
|
||||
> 本项目可独立运行,也可作为MES的插件使用。
|
||||
>
|
||||
> 下面章节简要介绍如何将代码合并入MES中。
|
||||
>
|
||||
> 需要保证timescaledb的版本在2.2.0或以上。
|
||||
48
docs/src/scada/service.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# 后端配置
|
||||
|
||||
添加pg扩展(Ubuntu):
|
||||
|
||||
```bash
|
||||
sudo apt install php-pgsql
|
||||
```
|
||||
|
||||
添加pg扩展(CentOS 7):
|
||||
|
||||
```bash
|
||||
sudo yum install php-pgsql
|
||||
```
|
||||
|
||||
添加pg扩展(CentOS8):
|
||||
|
||||
```bash
|
||||
sudo dnf install php-pgsql
|
||||
```
|
||||
|
||||
目录结构:
|
||||
|
||||
```pre
|
||||
📦EdgeManager
|
||||
┣ 📂EDataCapture
|
||||
┃ ┣ 📜EDataCapture.php
|
||||
┃ ┗ 📜ENodeConfigure.php
|
||||
┣ 📜Init.php
|
||||
┗ 📜Utils.php
|
||||
```
|
||||
|
||||
建议使用composer autoload引入。
|
||||
|
||||
将目录`EdgeManager`复制至项目后在composer.json内写入:
|
||||
|
||||
```json
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"EdgeManager\\": "EdgeManager/"
|
||||
},
|
||||
"files": [
|
||||
"EdgeManager/Utils.php",
|
||||
"EdgeManager/Init.php"
|
||||
]
|
||||
},
|
||||
```
|
||||
|
||||
运行`composer dump-autoload`即可正常调用EdgeManager的代码。
|
||||
BIN
docs/src/scada/sql_body.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
docs/src/scada/sql_head.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
43
docs/src/system.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# 系统配置
|
||||
|
||||
> 建议在开发环境、测试环境和生产环境均应用该页面中建议的设置。
|
||||
>
|
||||
> 如果使用集群,则每台机器都应应用该页面中建议的设置。
|
||||
|
||||
## OS安装
|
||||
|
||||
关键参数:
|
||||
|
||||
1. Ubuntu Server 22.04.1 LTS
|
||||
2. minimized安装
|
||||
3. 磁盘挂载不要选LVM方式
|
||||
4. 关闭swap(`/etc/fstab`去掉对应行后重启生效)
|
||||
|
||||
## 更改时区为一致
|
||||
|
||||
```bash
|
||||
sudo timedatectl set-timezone Asia/Shanghai
|
||||
```
|
||||
|
||||
## 内核调优
|
||||
|
||||
运行以下命令:
|
||||
|
||||
```bash
|
||||
echo '
|
||||
net.ipv4.tcp_max_tw_buckets = 20000
|
||||
net.core.somaxconn = 65535
|
||||
net.ipv4.tcp_max_syn_backlog = 262144
|
||||
net.core.netdev_max_backlog = 30000
|
||||
fs.file-max = 6815744
|
||||
net.netfilter.nf_conntrack_max = 2621440
|
||||
' | sudo tee -a /etc/sysctl.conf
|
||||
echo '
|
||||
* soft nofile 1024000
|
||||
* hard nofile 1024000
|
||||
root soft nofile 1024000
|
||||
root hard nofile 1024000
|
||||
' | sudo tee -a /etc/security/limits.conf
|
||||
```
|
||||
|
||||
然后`sudo reboot`重启服务器。
|
||||
145
docs/src/tests/cluster.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 集群搭建
|
||||
|
||||
> 操作系统的安装和配置请参照[系统配置](../system.md)章节。
|
||||
|
||||
## hostname配置
|
||||
|
||||
可通过`hostname`命令检查当前服务器的hostname。
|
||||
|
||||
在一切开始之前,首先要确保4台物理机的hostname分别为:`an`、`dn0`、`dn1`和`dn2`。
|
||||
|
||||
其中,`an`代表多节点TimescaleDB集群中的Access Node(接入节点),`dnx`则代表Data Node(数据节点)。
|
||||
|
||||
在安装操作系统时,可以一并设置hostname。如果有改变hostname的需要,可以类似这样操作:
|
||||
|
||||
```bash
|
||||
sudo hostnamectl set-hostname an
|
||||
```
|
||||
|
||||
则hostname会被改为`an`(**即时生效**)。
|
||||
|
||||
## hosts配置
|
||||
|
||||
可通过编辑`/etc/hosts`文件,将局域网内hostname和IP地址的映射关系写入,此处不赘述具体步骤。
|
||||
|
||||
> 需确保集群内每一节点都可以**正确地**将其他节点的hostname解析为对应的IP地址。
|
||||
|
||||
## MicroK8s安装
|
||||
|
||||
```bash
|
||||
sudo snap install microk8s --classic --channel=1.25
|
||||
```
|
||||
|
||||
> MicroK8s安装也可以在Ubuntu安装过程勾选完成。
|
||||
>
|
||||
> 1.25版本对最新内核的cgroup v2识别可能有问题(`microk8s.inspect`会[提示warning](https://github.com/canonical/microk8s/issues/3375)),忽略即可,并不影响集群稳定性。
|
||||
|
||||
将当前用户加入`microk8s`用户组方便管理:
|
||||
|
||||
```bash
|
||||
sudo usermod -a -G microk8s $USER
|
||||
sudo chown -f -R $USER ~/.kube
|
||||
newgrp microk8s
|
||||
```
|
||||
|
||||
gcr镜像无法在祖国大陆直接获取,[使用阿里云替代](https://microk8s.io/docs/registry-private#configure-registry-mirrors-7),但截至该文档编写时,官方文档提供的解决方法[有问题](https://github.com/canonical/microk8s/issues/472#issuecomment-1256947878),可以用下述方法替代:
|
||||
|
||||
```bash
|
||||
sed -i 's#k8s.gcr.io#registry.aliyuncs.com/google_containers#g' /var/snap/microk8s/current/args/containerd-template.toml
|
||||
sudo snap restart microk8s
|
||||
```
|
||||
|
||||
确认K8s是否已经正常运行:
|
||||
|
||||
```bash
|
||||
microk8s.status -w
|
||||
```
|
||||
|
||||
如无异常,片刻后会显示如下内容:
|
||||
|
||||
```pre
|
||||
microk8s is running
|
||||
high-availability: no
|
||||
datastore master nodes: 127.0.0.1:19001
|
||||
datastore standby nodes: none
|
||||
addons:
|
||||
enabled:
|
||||
ha-cluster # (core) Configure high availability on the current node
|
||||
disabled:
|
||||
community # (core) The community addons repository
|
||||
dashboard # (core) The Kubernetes dashboard
|
||||
dns # (core) CoreDNS
|
||||
gpu # (core) Automatic enablement of Nvidia CUDA
|
||||
helm # (core) Helm 2 - the package manager for Kubernetes
|
||||
helm3 # (core) Helm 3 - Kubernetes package manager
|
||||
host-access # (core) Allow Pods connecting to Host services smoothly
|
||||
hostpath-storage # (core) Storage class; allocates storage from host directory
|
||||
ingress # (core) Ingress controller for external access
|
||||
mayastor # (core) OpenEBS MayaStor
|
||||
metallb # (core) Loadbalancer for your Kubernetes cluster
|
||||
metrics-server # (core) K8s Metrics Server for API access to service metrics
|
||||
prometheus # (core) Prometheus operator for monitoring and logging
|
||||
rbac # (core) Role-Based Access Control for authorisation
|
||||
registry # (core) Private image registry exposed on localhost:32000
|
||||
storage # (core) Alias to hostpath-storage add-on, deprecated
|
||||
```
|
||||
|
||||
此外,为了方便拉取`docker.io`上的镜像,最好配置使用[阿里云的镜像加速器](https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors):
|
||||
|
||||
```bash
|
||||
echo '
|
||||
server = "https://registry-1.docker.io"
|
||||
|
||||
[host."https://r3vfmoy2.mirror.aliyuncs.com"]
|
||||
capabilities = ["pull", "resolve"]
|
||||
' | tee /var/snap/microk8s/current/args/certs.d/docker.io/hosts.toml
|
||||
sudo snap restart microk8s
|
||||
```
|
||||
|
||||
重复上述步骤使得MicroK8s在每台服务器上得以妥善安装配置。
|
||||
|
||||
## 组建集群
|
||||
|
||||
组建集群的详细步骤可以参考[官方文档](https://microk8s.io/docs/clustering),但需**注意**:
|
||||
|
||||
1. `microk8s join`之前,需要在每个node的`/etc/hosts`中加入其他node的信息
|
||||
2. 每个节点加入后,token会过期,需要再次`microk8s add-node`获取新的token
|
||||
|
||||
## 启用Helm3扩展
|
||||
|
||||
> Helm3扩展自MicroK8s v1.25开始已经默认开启,无需手动安装。
|
||||
|
||||
同样是祖国大陆受GFW限制的问题,如果直接`microk8s.enable helm3`是从官方拉取文件安装Helm3,速度极慢且中途易出现下载中断。
|
||||
|
||||
可以把对应的安装脚本里的文件位置指向华为云的镜像:
|
||||
|
||||
```bash
|
||||
sed 's#https://get.helm.sh#https://mirrors.huaweicloud.com/helm/v3.8.0#' /var/snap/microk8s/common/addons/core/addons/helm3/enable | SNAP=/snap/microk8s/current SNAP_DATA=/var/snap/microk8s/current SNAP_ARCH=amd64 bash
|
||||
```
|
||||
|
||||
运行完毕后再执行`microk8s.status`即可看到Helm3扩展已启用。
|
||||
|
||||
## 后续配置
|
||||
|
||||
由于我们要使用Helm来作为包管理器,而helm会读取K8s的`config`文件,如果不加设置,默认会使用`/var/snap/microk8s/current/credentials/client.config`,但由于安全性问题,会被警告:
|
||||
|
||||
```pre
|
||||
WARNING: Kubernetes configuration file is group-readable. This is insecure.
|
||||
```
|
||||
|
||||
所以我们需要创建用户级别的配置文件并赋予正确的文件权限:
|
||||
|
||||
```bash
|
||||
microk8s config > ~/.kube/config
|
||||
chmod go-r ~/.kube/config
|
||||
```
|
||||
|
||||
为便于操作,再将常用命令做一下`alias`:
|
||||
|
||||
```bash
|
||||
echo '
|
||||
alias kubectl="microk8s kubectl"
|
||||
alias helm="microk8s helm3 --kubeconfig ~/.kube/config"
|
||||
' | tee -a ~/.bash_aliases
|
||||
```
|
||||
|
||||
89
docs/src/tests/details.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# 技术细节
|
||||
|
||||
## 0. 如何确保集群的access-node/data-node pods运行于我们期望的物理节点上?
|
||||
|
||||
首先要明确一个概念,由于需要对Access Node和Data Node进行区分,TimescaleDB集群其实是一个有状态的应用。
|
||||
|
||||
有状态应用借助K8s的[StatefulSet workload](https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/)可以很便捷的部署。
|
||||
|
||||
但很久以前TimescaleDB官方维护的multinodes版本Helm仓库里对于Access Node的节点选择并没有清晰定义。
|
||||
|
||||
[`charts/templates/statefulset-timescaledb-accessnode.yaml`](https://github.com/timescale/helm-charts/blob/29b589c96821a3f9ffe890cd775d7da471007aa0/charts/timescaledb-multinode/templates/statefulset-timescaledb-accessnode.yaml#L117-L118):
|
||||
|
||||
```yaml
|
||||
{{- with .Values.nodeSelector }}
|
||||
nodeSelector:
|
||||
```
|
||||
|
||||
[`charts/values.yaml`](https://github.com/timescale/helm-charts/blob/29b589c96821a3f9ffe890cd775d7da471007aa0/charts/timescaledb-multinode/values.yaml#L91):
|
||||
|
||||
```yaml
|
||||
nodeSelector: {}
|
||||
```
|
||||
|
||||
咱就是说,直接就摆烂了。
|
||||
|
||||
这直接导致多个接入节点和数据节点的pods很大概率错误地尝试分配在同一个物理节点上,由于资源冲突,最终造成随机的某(几)个节点创建失败。
|
||||
|
||||
按照[我们之前对hostname的定义](cluster.md#hostname配置),最后将`charts/templates/statefulset-timescaledb-accessnode.yaml`里对应部分改为:
|
||||
|
||||
```yaml
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: an
|
||||
```
|
||||
|
||||
解决了这个问题。
|
||||
|
||||
`kubernetes.io/hostname`是K8s的一个保留label,通过如下命令可以查看:
|
||||
|
||||
```bash
|
||||
kubectl get no --show-labels=true
|
||||
```
|
||||
|
||||

|
||||
|
||||
这里有一个复杂情况,如果设定的Data Node数目超过了节点数目,亦或是其中某个节点挂掉,集群的Access Node或者Data Node是会尝试漂移到某个节点上的,也就是某些节点上会启动多于一个TimescaleDB实例。这是我们不想看到的。
|
||||
|
||||
在Helm chart中妥善设置节点亲和性可以有效解决这个问题:
|
||||
|
||||
`charts/templates/statefulset-timescaledb-accessnode.yaml`:
|
||||
|
||||
```yaml
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- an
|
||||
```
|
||||
|
||||
`charts/templates/statefulset-timescaledb-datanode.yaml`:
|
||||
|
||||
```yaml
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: NotIn
|
||||
values:
|
||||
- an
|
||||
```
|
||||
|
||||
核心思想是使用`requiredDuringSchedulingIgnoredDuringExecution`替代`preferredDuringSchedulingIgnoredDuringExecution`策略,以实现强制性的调度[^note]。
|
||||
|
||||
[^note]: <https://kubernetes.io/zh-cn/docs/tasks/configure-pod-container/assign-pods-nodes-using-node-affinity/#schedule-a-Pod-using-required-node-affinity>
|
||||
|
||||
## 1. TimescaleDB集群的多节点间操作是如何实现免密码认证的?
|
||||
|
||||
PostgreSQL有一个[Password file](https://www.postgresql.org/docs/current/libpq-pgpass.html)的设定,通过一个`.pgpass`来进行一些免密认证。
|
||||
|
||||
TimescaleDB可以复用这个文件,但是必须在PostgreSQL的配置文件中以[`timescaledb.passfile`](https://docs.timescale.com/timescaledb/latest/how-to-guides/configuration/timescaledb-config/#timescaledb-passfile-string)字段的形式提供。
|
||||
|
||||
打个比方,应用层中使用[`create-distributed-hypertable()`](https://docs.timescale.com/api/latest/distributed-hypertables/create_distributed_hypertable/#create-distributed-hypertable)函数创建分布式超表的时候,其是不允许将密码传参提供的,这个时候就一定要环境中提供免密认证,使得各个nodes之间可以自由连通。
|
||||
|
||||
而[`add_data_node()`](https://docs.timescale.com/api/latest/distributed-hypertables/add_data_node/)函数则可以直接传参指定密码,因其为环境部署过程中可能需要的。但是出于安全性考虑,也**不建议**这样做。
|
||||
BIN
docs/src/tests/get_all.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
BIN
docs/src/tests/node_label.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/src/tests/psql.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
33
docs/src/tests/questions/failed_pod.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# Pods创建失败
|
||||
|
||||
## 问题描述
|
||||
|
||||
使用`kubectl get all -l release=tsdb` 发现有单个数据节点或`(1个数据节点 + 1个接入节点)`的pods的状态是某种error,`kubectl describe`之后发现类似下面的报错:
|
||||
|
||||
```pre
|
||||
Events:
|
||||
Type Reason Age From Message
|
||||
---- ------ ---- ---- -------
|
||||
Normal Pulled 4m34s (x2 over 4m48s) kubelet Container image "timescale/timescaledb-ha:pg14.5-ts2.8.0-patroni-static-primary-latest" already present on machine
|
||||
...
|
||||
Warning MissingClusterDNS 7s (x8 over 39s) kubelet pod: "tsdb-timescaledb-data-1_default(62ba2d9e-6cff-4446-aa2b-ae46f5948997)". kubelet does not have ClusterDNS IP configured and cannot create Pod using "ClusterFirst" policy. Falling back to "Default" policy.
|
||||
```
|
||||
|
||||
## 解决方案
|
||||
|
||||
很容易误会是没有启用`core/dns`扩展所致,但事实上我们已经[启用了](../tsdb.md#安装必要扩展addons)改扩展。
|
||||
|
||||
`charts/templates/svc-timescaledb-data.yaml`文件中的相关定义为:
|
||||
|
||||
```yaml
|
||||
spec:
|
||||
clusterIP: None
|
||||
```
|
||||
|
||||
而K8s其实是允许定义Headless Services的[^note],就是通过将`.spec.clusterIP`指定为`"None"`。
|
||||
|
||||
所以问题其实是:**没有能够正确应用Selector把pod分配到正确的节点上**。
|
||||
|
||||
具体的设置方法可参照[技术细节](../details.md#0-如何确保集群的access-node-pod运行于我们设定的物理节点上)。
|
||||
|
||||
[^note]: <https://kubernetes.io/docs/concepts/services-networking/service/#headless-services>
|
||||
3
docs/src/tests/questions/questions.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 常见问题
|
||||
|
||||
本章节用于记录测试过程中常见问题及解决方案。
|
||||
38
docs/src/tests/questions/ram.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 使用物理内存作持久化存储
|
||||
|
||||
首先划分一块足够的内存空间并挂载:
|
||||
|
||||
```bash
|
||||
sudo mkdir -pm777 /mnt/ramdisk
|
||||
sudo mount -t ramfs -o size=10G ramdisk /mnt/ramdisk
|
||||
```
|
||||
|
||||
> **需要在每个节点上都如法炮制**。
|
||||
|
||||
创建新的StorageClass:
|
||||
|
||||
```bash
|
||||
kubectl apply -f ram-hostpath-sc.yaml
|
||||
```
|
||||
|
||||
重新启动集群服务的时候,使用命令行传参的方式覆盖默认(`values.yaml`)设置。
|
||||
|
||||
```bash
|
||||
helm install tsdb --set persistentVolume.size=8G --set persistentVolume.storageClass=ram-hostpath .
|
||||
```
|
||||
|
||||
Microk8s的hostpath storage有bug,新创建的PV权限并非`777`,会导致StatefulSet的pods全部启动失败。暂时只能在每个节点上手动设置权限:
|
||||
|
||||
```bash
|
||||
sudo chmod -R 777 /mnt/ramdisk/
|
||||
```
|
||||
|
||||
之后静等对应的pods自动重启。
|
||||
|
||||
[移除集群](../tsdb.html#移除集群)的操作不再赘述。
|
||||
|
||||
如要清除创建的StorageClass:
|
||||
|
||||
```bash
|
||||
kubectl delete sc/ram-hostpath
|
||||
```
|
||||
BIN
docs/src/tests/questions/sandbox.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
27
docs/src/tests/questions/terminating.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Pods无法清除
|
||||
|
||||
## 问题描述
|
||||
|
||||
执行`helm uninstall`或者`kubectl delete`等命令的时候发现pods无法删除:
|
||||
|
||||

|
||||
|
||||
使用`kubectl describe`观察详情的时候发现通讯失败(但Sandbox其实已经被清除了):
|
||||
|
||||

|
||||
|
||||
## 解决方案
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
for p in $(kubectl get pods | grep Terminating | awk '{print $1}'); do kubectl delete pod $p --grace-period=0 --force; done
|
||||
```
|
||||
|
||||
如果运行上面的命令后仍有pod处于`Unknown`状态,大胆清除[^note]:
|
||||
|
||||
```bash
|
||||
kubectl patch pod <pod> -p '{"metadata":{"finalizers":null}}'
|
||||
```
|
||||
|
||||
[^note]: <https://kubernetes.io/docs/tasks/run-application/force-delete-stateful-set-pod/#delete-pods>
|
||||
BIN
docs/src/tests/questions/terminating.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
docs/src/tests/show_nodes.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
docs/src/tests/timescaledb-multi.png
Normal file
|
After Width: | Height: | Size: 165 KiB |
115
docs/src/tests/tsdb.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# 运行多节点TimescaleDB服务
|
||||
|
||||
在[上一章节](cluster.md)中,我们完成了基本的K8s集群搭建并启用了Helm3扩展以便于利用helm charts便捷部署应用于其上。
|
||||
|
||||
接下来说明如何配置一个测试用集群。
|
||||
|
||||
> 注意,生产环境中的配置可以参考本章,但不应照搬;
|
||||
>
|
||||
> 本章中的配置仅提供**最简易**环境并论证运维**可行性**,性能和稳定性方面均**无法胜任生产环境**。
|
||||
|
||||
## 安装必要扩展(Addons)
|
||||
|
||||
运行以下命令,并耐心等待自动配置完成:
|
||||
|
||||
```bash
|
||||
microk8s enable dns hostpath-storage metallb
|
||||
```
|
||||
|
||||
`dns`是必需的,因为Data Nodes上运行的TimescaleDB实例作为[Headless Services](questions/failed_pod.md#解决方案),本身是没有为它们提供IP的。如果没有K8s内建的DNS服务,根本就无法根据pod name获取到具体的IP,也就是说这些pods间无法通讯,数据库集群会创建失败。
|
||||
|
||||
其中,`hostpath-storage`是microk8s自带的一个PV(**P**ersistent**V**olumes,持久化存储)插件,其为开发者提供了动态供应(dynamic provisioning)的PV。
|
||||
|
||||
简单来讲,用户通过helm charts或其他方式每创建一个带有PVC(**P**ersistent**V**olume**C**laim)的pod,动态供应者(dynamic provisioner)就会为其自动创建一个可用的PV。
|
||||
|
||||
> **禁止在生产环境中使用hostpath-storage!**
|
||||
>
|
||||
> 由hostpath-storage提供的PV并不支持数据在节点间迁移,所以只能用于开发/测试环境。
|
||||
|
||||
`metallb`则是轻量级的为bare-metal服务器上提供负载均衡的解决方案。
|
||||
|
||||
> bare-metal特指比如IoT场景这种局域网中互连的服务器们,即区别于“云上”的虚拟服务器(**V**irtual **P**rivate **S**erver, VPS)。
|
||||
|
||||
启用metallb扩展时,命令行会提示输入IP网段来创建一个地址池,当service类型为`LoadBalancer`时,metallb会从已创建的地址池中挑选一个IP来作为服务的`EXTERNAL-IP`。
|
||||
|
||||
> 应为metallb提供**与host同一层的**IP地址,因其可以直接在局域网内被直接访问,而无需额外的DNS服务。
|
||||
>
|
||||
> 但**不应重复使用节点的IP**,会导致network traffic,造成节点间无法通讯,集群不能正常工作。
|
||||
>
|
||||
> 这也为**部署于K8s集群外的应用**(比如EdgeManager)提供了访问集群内服务的支撑。
|
||||
|
||||
指定网段时可以使用`-`符号连接两个IP地址来表示一个区间。比如也可以使用这种方式在启用扩展时直接创建地址池:
|
||||
|
||||
```bash
|
||||
microk8s.enable metallb:192.168.0.198-192.168.0.199
|
||||
```
|
||||
|
||||
运行完毕后,`192.168.0.198`和`192.168.0.199`均会被加入地址池中。metallb一般会挑选第一个,当第一个地址不可用时则顺序替换为下一个地址。
|
||||
|
||||
## 启动集群
|
||||
|
||||
先拉取代码仓库:
|
||||
|
||||
```bash
|
||||
git pull http://118.195.187.246:10030/ysun/EdgeManager.git
|
||||
```
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
# 进入位于项目根目录的charts目录内
|
||||
cd EdgeManager/charts
|
||||
helm install tsdb .
|
||||
```
|
||||
|
||||
## 移除集群
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
helm uninstall tsdb
|
||||
```
|
||||
|
||||
但`helm uninstall`并不会丧心病狂地删除持久化的数据,因此如果想**彻底删除**这些数据,可以用以下命令删除PVC:
|
||||
|
||||
```bash
|
||||
kubectl delete $(kubectl get pvc -l release=tsdb -o name)
|
||||
```
|
||||
|
||||
稍后对应的PV也会被清除(因为我们之前使用的PV插件`hostpath-storage`是dynamic provisioner)。
|
||||
|
||||
> 如果需要重新部署服务,建议使用`kubectl get pv`确定PV已经完全被释放后再进行。
|
||||
|
||||
## 验证集群状态
|
||||
|
||||
运行:
|
||||
|
||||
```bash
|
||||
kubectl get all -l release=tsdb
|
||||
```
|
||||
|
||||

|
||||
|
||||
图中可见,`192.168.0.198`即为metallb提供的可供集群外部应用访问的IP。
|
||||
|
||||
验证Access Node上是否成功加入了数据节点:
|
||||
|
||||
```bash
|
||||
kubectl exec -ti $(kubectl get pod -l timescaleNodeType=access -o name) -c timescaledb -- psql -d scada -c 'select node_name from timescaledb_information.data_nodes'
|
||||
```
|
||||
|
||||

|
||||
|
||||
尝试从另外一台物理机访问该多节点TimescaleDB集群:
|
||||
|
||||

|
||||
|
||||
至此,集群已经可以正常使用了。
|
||||
|
||||
## 部署图
|
||||
|
||||
该测试环境的部署结构[^note]为:
|
||||
|
||||

|
||||
|
||||
[^note]: <https://docs.timescale.com/timescaledb/latest/overview/timescale-kubernetes/#multi-node-distributed-timescaledb>
|
||||
127
src/api/modules/edgeServer.api.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { handlePost } from '../tools'
|
||||
|
||||
export default ({ service, request, serviceForMock, requestForMock, mock, faker, tools }) => ({
|
||||
/**
|
||||
* @description 方法名称
|
||||
* @param {Object} data 请求携带的信息
|
||||
*/
|
||||
VERIFY_SERVER (url) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
method: 'post',
|
||||
url: url + '/Admin/ServerSettingsRequest'
|
||||
})
|
||||
},
|
||||
|
||||
MODIFY_SERVER (url, data) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
data: data,
|
||||
method: 'post',
|
||||
url: url + '/Admin/ServerSettingsModify'
|
||||
})
|
||||
},
|
||||
|
||||
ADD_SERVER: (data) => handlePost(request, data),
|
||||
UPDATE_SERVER: (data) => handlePost(request, data),
|
||||
REMOVE_SERVER: (data) => handlePost(request, data),
|
||||
|
||||
QUERY_SERVERS () {
|
||||
return request({ url: '?query=servers' })
|
||||
},
|
||||
|
||||
QUERY_SERVE_MONITORING () {
|
||||
return request({ url: '?query=working_subclasses' })
|
||||
},
|
||||
GET_DEVICE (serveId) {
|
||||
return request({ url: '?query=device_list&id=' + serveId })
|
||||
},
|
||||
GET_DEVICE_STATUS (serveId) {
|
||||
return request({ url: '?query=device_status&id=' + serveId })
|
||||
},
|
||||
ADD_DEVICE: (data) => handlePost(request, data),
|
||||
DEL_DEVICE: (data) => handlePost(request, data),
|
||||
GET_HSLSERVER_CONFIGURE (url) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
method: 'post',
|
||||
url: url + '/Admin/JsonSettingsRequest'
|
||||
})
|
||||
},
|
||||
SET_HSLSERVER_CONFIGURE (url, data) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
data: data,
|
||||
method: 'post',
|
||||
url: url + '/Admin/JsonSettingsModify'
|
||||
})
|
||||
},
|
||||
SET_DEVICE_CONFIGURE: (data) => handlePost(request, data),
|
||||
GET_DEVICE_CONFIGURE (id) {
|
||||
return request({ url: '?query=device&id=' + id })
|
||||
},
|
||||
GET_SERVE_DEVICE_MONITORING (url) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
url: url + '/Edge/DeviceData?data=/'
|
||||
})
|
||||
},
|
||||
GET_ALL_DEVICES () {
|
||||
return request({ url: '?query=all_devices' })
|
||||
},
|
||||
SET_SERVER_EXEC: (data) => handlePost(request, data),
|
||||
GET_SERVER_DEVICE_STATUS (url) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
url: url + '/Edge/DeviceData?data=__status'
|
||||
})
|
||||
},
|
||||
GET_DEVICE_POINT_VALUE (url, deviceName, data, type) {
|
||||
return request({
|
||||
validateStatus: (status) => {
|
||||
return status === 404 ? 200 : status
|
||||
},
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
data: {
|
||||
address: data
|
||||
},
|
||||
method: 'post',
|
||||
url: url + '/' + deviceName + '/Read' + type + '32'
|
||||
})
|
||||
},
|
||||
SET_DEVICE_POINT_VALUE (url, deviceName, data, type, value) {
|
||||
return request({
|
||||
auth: {
|
||||
username: 'admin',
|
||||
password: process.env.VUE_APP_HSLSERVER_PASSWORD
|
||||
},
|
||||
data: {
|
||||
address: data,
|
||||
value: value
|
||||
},
|
||||
method: 'post',
|
||||
url: url + '/' + deviceName + '/Write' + type + '32'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -1,8 +1,4 @@
|
||||
const handlePost = (request, data) => (request({
|
||||
url: '',
|
||||
method: 'post',
|
||||
data
|
||||
}))
|
||||
import { handlePost } from '../tools'
|
||||
|
||||
export default ({ service, request, serviceForMock, requestForMock, mock, faker, tools }) => ({
|
||||
/**
|
||||
@@ -14,7 +10,7 @@ export default ({ service, request, serviceForMock, requestForMock, mock, faker,
|
||||
UPDATE_NODE: (data) => handlePost(request, data),
|
||||
REMOVE_NODE: (data) => handlePost(request, data),
|
||||
|
||||
QUERY_NODE () {
|
||||
QUERY_NODES () {
|
||||
return request({ url: '?query=nodes' })
|
||||
},
|
||||
|
||||
|
||||
@@ -84,3 +84,9 @@ export function errorCreate (msg) {
|
||||
errorLog(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
export const handlePost = (request, data) => (request({
|
||||
url: '',
|
||||
method: 'post',
|
||||
data
|
||||
}))
|
||||
|
||||
@@ -23,5 +23,12 @@ export const menuAside = supplementPath([
|
||||
{ path: '/scada_configure', title: 'SCADA节点配置' },
|
||||
{ path: '/scada_query', title: 'SCADA数据查询' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: '采集服务管理',
|
||||
children: [
|
||||
{ path: '/edge_server_configure', title: '服务配置' },
|
||||
{ path: '/edge_server_monitor', title: '服务监控' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
@@ -39,6 +39,24 @@ const frameIn = [
|
||||
},
|
||||
component: _import('scada/scadaQuery')
|
||||
},
|
||||
{
|
||||
path: 'edge_server_configure',
|
||||
name: 'edge_server_configure',
|
||||
meta: {
|
||||
title: '服务配置',
|
||||
auth: true
|
||||
},
|
||||
component: _import('edgeServer/edgeServerConfigure')
|
||||
},
|
||||
{
|
||||
path: 'edge_server_monitor',
|
||||
name: 'edge_server_monitor',
|
||||
meta: {
|
||||
title: '服务监控',
|
||||
auth: true
|
||||
},
|
||||
component: _import('edgeServer/edgeServerMonitor')
|
||||
},
|
||||
// 系统 前端日志
|
||||
{
|
||||
path: 'log',
|
||||
|
||||
886
src/views/edgeServer/edgeServerConfigure/device.vue
Normal file
@@ -0,0 +1,886 @@
|
||||
<template>
|
||||
<el-container>
|
||||
<el-aside width="15%">
|
||||
<div class="menu-box">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" class="menu-header">
|
||||
<div class="header-title">设备</div>
|
||||
<el-dropdown size="small" style="vertical-align: middle" trigger="click" @command="handleCommand">
|
||||
<el-button type="primary" round>
|
||||
<i class="el-icon-s-tools" style="font-size: 14px"></i><i class="el-icon-arrow-down el-icon--right"
|
||||
style="font-size: 14px"></i>
|
||||
</el-button>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="add">新加设备</el-dropdown-item>
|
||||
<el-dropdown-item command="del">删除设备</el-dropdown-item>
|
||||
<el-dropdown-item>设备重启</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload"
|
||||
action=""
|
||||
:before-upload="importDeviceConfigure">
|
||||
导入配置
|
||||
</el-upload>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item command="export" >导出配置</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<el-menu :style="{ borderRight: 'none' }" :defaultActive="deviceActiveStatus" @select="getDeviceConfigure">
|
||||
<el-menu-item class="serve-item" shadow="always" v-for="(item, i) in deviceData" :key="i" :index="i + ''">
|
||||
<div style="position: relative">
|
||||
<div style="display: inline-block">{{ item.name }}</div>
|
||||
<div :class="{
|
||||
'device-status': true,
|
||||
'device-status-online': item.status,
|
||||
'device-status-offline': !item.status,
|
||||
}"></div>
|
||||
</div>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</el-card>
|
||||
</div>
|
||||
</el-aside>
|
||||
<el-main :style="{ padding: '10px' }">
|
||||
<el-card class="box-card">
|
||||
<div slot="header" style="position: relative">
|
||||
<div class="header-title">设备配置</div>
|
||||
<el-button class="header-button" type="primary" @click="setDeviceConfigureConfirm">应用</el-button>
|
||||
</div>
|
||||
<device-configure ref="deviceConfigure" :defaultDeviceName="defaultDeviceName"
|
||||
:defaultFormData="defaultFormData" />
|
||||
</el-card>
|
||||
<el-card class="box-card" style="margin-top: 5px">
|
||||
<d2-crud ref="d2Crud"
|
||||
:columns="columns"
|
||||
:data="devicePointTableData"
|
||||
:rowHandle="rowHandle"
|
||||
add-title="新增"
|
||||
edit-title="绑定"
|
||||
:add-template="addTemplate"
|
||||
:add-rules="addRules"
|
||||
:form-options="formOptions"
|
||||
@row-add="addDevicePoint"
|
||||
@row-edit="devicePointBandingNode"
|
||||
@row-remove="delDevicePoint"
|
||||
@cpoy='copyDevicePoint'
|
||||
@dialog-cancel="handleDialogCancel"
|
||||
@banding="bandingNodeTemplate"
|
||||
@cell-data-change="handleCellDataChange"
|
||||
@form-data-change="handleFormDataChange"
|
||||
>
|
||||
<div slot="header">
|
||||
<el-button style="margin-bottom: 5px" @click="addRow">新增</el-button>
|
||||
<span class="demo-form-inline" style="margin-left:15px;">
|
||||
筛选:<el-input placeholder="请输入筛选内容" v-model="nameSearchValue" style="width:300px" class="input-with-select"></el-input>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</d2-crud>
|
||||
</el-card>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { each, assign, unset, filter, isArray, map, capitalize, split, toUpper, replace } from 'lodash'
|
||||
const DataTypeCodeOptions = [
|
||||
{
|
||||
value: 'string',
|
||||
label: 'string (字符串)'
|
||||
},
|
||||
{
|
||||
value: 'int',
|
||||
label: 'int (整数)'
|
||||
},
|
||||
{
|
||||
value: 'float',
|
||||
label: 'float (浮点数)'
|
||||
},
|
||||
{
|
||||
value: 'bool',
|
||||
label: 'bool (逻辑值)'
|
||||
},
|
||||
{
|
||||
value: 'uint',
|
||||
label: 'uint'
|
||||
},
|
||||
{
|
||||
value: 'short',
|
||||
label: 'short'
|
||||
},
|
||||
{
|
||||
value: 'ushort',
|
||||
label: 'ushort'
|
||||
},
|
||||
{
|
||||
value: 'long',
|
||||
label: 'long'
|
||||
},
|
||||
{
|
||||
value: 'ulong',
|
||||
label: 'ulong'
|
||||
},
|
||||
{
|
||||
value: 'double',
|
||||
label: 'double'
|
||||
},
|
||||
{
|
||||
value: 'byte',
|
||||
label: 'byte'
|
||||
},
|
||||
{
|
||||
value: 'sbyte',
|
||||
label: 'sbyte'
|
||||
}
|
||||
]
|
||||
|
||||
export default {
|
||||
props: {
|
||||
server: {
|
||||
default: {
|
||||
id: 0,
|
||||
url: '',
|
||||
port: ''
|
||||
}
|
||||
}
|
||||
},
|
||||
components: {
|
||||
DeviceConfigure: () => import('./deviceConfigure')
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
active: '',
|
||||
columns: [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
key: '@Name',
|
||||
'show-overflow-tooltip': true,
|
||||
component: {
|
||||
size: 'small'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '绑定',
|
||||
key: '@Binding'
|
||||
},
|
||||
{
|
||||
title: '地址',
|
||||
key: '@Address',
|
||||
component: {
|
||||
name: 'el-input',
|
||||
size: 'small'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
key: '@DataTypeCode',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
size: 'small',
|
||||
options: DataTypeCodeOptions
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '采集周期',
|
||||
key: '@RequestInterval',
|
||||
component: {
|
||||
name: 'el-input',
|
||||
size: 'small'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '长度',
|
||||
key: '@Length',
|
||||
width: '80%',
|
||||
component: {
|
||||
name: 'el-input',
|
||||
size: 'small'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '说明',
|
||||
key: '@Description',
|
||||
component: {
|
||||
name: 'el-input',
|
||||
size: 'small'
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '当前值',
|
||||
key: '@Value',
|
||||
width: '80%',
|
||||
component: {
|
||||
name: 'el-input',
|
||||
size: 'small'
|
||||
}
|
||||
}
|
||||
],
|
||||
rowHandle: {
|
||||
minWidth: '150',
|
||||
custom: [
|
||||
{
|
||||
text: '绑定',
|
||||
size: 'mini',
|
||||
emit: 'banding',
|
||||
show (index, row) {
|
||||
if (row.showBindButton) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
{
|
||||
text: '复制',
|
||||
size: 'mini',
|
||||
emit: 'cpoy',
|
||||
show (index, row) {
|
||||
if (row.showCopyButton) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
],
|
||||
remove: {
|
||||
size: 'mini',
|
||||
confirm: true,
|
||||
show (index, row) {
|
||||
if (row.showRemoveButton) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
},
|
||||
formOptions: {
|
||||
modal: false,
|
||||
saveLoading: false
|
||||
},
|
||||
addTemplate: {
|
||||
'@Name': {
|
||||
title: '名称'
|
||||
},
|
||||
'@Address': {
|
||||
title: '地址'
|
||||
},
|
||||
'@DisplayName': {
|
||||
title: '显示名称'
|
||||
},
|
||||
'@RequestType': {
|
||||
title: '请求类型'
|
||||
},
|
||||
'@DataTypeCode': {
|
||||
title: '数据类型',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
options: DataTypeCodeOptions,
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
'@RequestInterval': {
|
||||
title: '采集周期(ms)'
|
||||
},
|
||||
'@Length': {
|
||||
title: '长度'
|
||||
},
|
||||
'@Description': {
|
||||
title: '说明'
|
||||
}
|
||||
},
|
||||
addRules: {
|
||||
'@Name': [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'@Address': [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入地址',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'@DisplayName': [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入显示名称',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'@RequestType': [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请输入请求类型',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'@DataTypeCode': [
|
||||
{
|
||||
required: true,
|
||||
type: 'string',
|
||||
message: '请选择类型',
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
'@RequestInterval': [
|
||||
{ required: true, message: '请输入采集周期(ms)', trigger: 'blur' }
|
||||
]
|
||||
},
|
||||
timing: null,
|
||||
nameSearchValue: '',
|
||||
serverData: {},
|
||||
devicePointData: [],
|
||||
nodeCodeData: [],
|
||||
workingSubclasses: [],
|
||||
dialogVisible: false,
|
||||
deviceActiveStatus: '0',
|
||||
defaultDeviceName: '',
|
||||
defaultFormData: {},
|
||||
deviceData: [],
|
||||
deviceConfigure: {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
server: {
|
||||
handler (val) {
|
||||
this.serverData = val
|
||||
if (val.id) {
|
||||
this.getDevice()
|
||||
this.getDeviceStatus()
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
devicePointTableData () {
|
||||
if (this.nameSearchValue) {
|
||||
return this.devicePointData.filter((item) => {
|
||||
if (item['@Name'].search(this.nameSearchValue) > -1) {
|
||||
return item
|
||||
}
|
||||
})
|
||||
} else {
|
||||
return this.devicePointData
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRow () {
|
||||
this.$refs.d2Crud.showDialog({
|
||||
mode: 'add'
|
||||
})
|
||||
},
|
||||
handleFormDataChange ({ key }) {
|
||||
if (key === 'workingSubclass') {
|
||||
const { workingSubclass } = this.$refs.d2Crud.formData
|
||||
this.getCodesByWorkingSubclass(workingSubclass)
|
||||
this.$refs.d2Crud.$forceUpdate()
|
||||
}
|
||||
},
|
||||
handleCellDataChange ({ rowIndex, row, key, value }) {
|
||||
let type
|
||||
if (row['@DataTypeCode'][0] === 'u') {
|
||||
type = split(row['@DataTypeCode'], '')
|
||||
type[1] = toUpper(type[0] + type[1])
|
||||
type = type.join('')
|
||||
} else {
|
||||
type = capitalize(row['@DataTypeCode'])
|
||||
}
|
||||
if (key === '@Value') {
|
||||
this.$api.SET_DEVICE_POINT_VALUE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port,
|
||||
this.deviceData[this.deviceActiveStatus].name,
|
||||
row['@Address'],
|
||||
type,
|
||||
value
|
||||
).then(res => {
|
||||
if (!res.IsSuccess) {
|
||||
this.$message({
|
||||
message: res.Message,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
this.devicePointData[rowIndex] = row
|
||||
},
|
||||
bandingNodeTemplate ({ index }) {
|
||||
this.$refs.d2Crud.showDialog({
|
||||
mode: 'edit',
|
||||
rowIndex: index,
|
||||
template: {
|
||||
workingSubclass: {
|
||||
title: '工序编码',
|
||||
value: '',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
options: this.workingSubclasses,
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
nodeCode: {
|
||||
title: '节点名称',
|
||||
value: '',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
options: this.nodeCodeData,
|
||||
span: 12
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
handleDialogCancel (done) {
|
||||
this.$message({
|
||||
message: '用户取消保存',
|
||||
type: 'warning'
|
||||
})
|
||||
done()
|
||||
},
|
||||
handleCommand (command) {
|
||||
switch (command) {
|
||||
case 'add':
|
||||
this.addDevice()
|
||||
break
|
||||
case 'del':
|
||||
this.delDevice()
|
||||
break
|
||||
case 'export':
|
||||
this.exportDeviceConfigure()
|
||||
break
|
||||
}
|
||||
},
|
||||
async getCodesByWorkingSubclass (workingSubclass) {
|
||||
try {
|
||||
const nodeCode = await this.$api.QUERY_CODES(workingSubclass)
|
||||
const nodeCodeData = []
|
||||
each(nodeCode, (o) => {
|
||||
nodeCodeData.push({ value: o, label: o })
|
||||
})
|
||||
this.nodeCodeData = nodeCodeData
|
||||
this.$nextTick(() => {
|
||||
this.$refs.d2Crud.handleFormTemplateMode(
|
||||
'nodeCode'
|
||||
).component.options = this.nodeCodeData
|
||||
this.$refs.d2Crud.$forceUpdate()
|
||||
})
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async getworkingSubclasses () {
|
||||
try {
|
||||
const workingSubclasses = await this.$api.QUERY_WORKING_SUBCLASSES()
|
||||
const workingSubclassesData = []
|
||||
each(workingSubclasses, (o) => {
|
||||
workingSubclassesData.push({ value: o, label: o })
|
||||
})
|
||||
this.workingSubclasses = workingSubclassesData
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async getDevice () {
|
||||
try {
|
||||
this.deviceData = await this.$api.GET_DEVICE(this.serverData.id)
|
||||
if (this.deviceData.length > 0) {
|
||||
this.getDeviceConfigure(this.deviceActiveStatus)
|
||||
} else {
|
||||
this.devicePointData = [] // 如果没有设备,防止显示上一次打开的设备数据先清空数据
|
||||
this.defaultDeviceName = ''
|
||||
this.nameSearchValue = ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async getDeviceConfigure (e) {
|
||||
this.deviceActiveStatus = e
|
||||
this.devicePointData = [] // 当切换设备时把保存configure、point的数据清空
|
||||
this.defaultDeviceName = ''
|
||||
this.nameSearchValue = ''
|
||||
const deviceData = await this.$api.GET_DEVICE_CONFIGURE(
|
||||
this.deviceData[e].id
|
||||
)
|
||||
if (deviceData[0].conf) {
|
||||
const conf = JSON.parse(deviceData[0].conf)
|
||||
if (conf.DeviceTypeName !== undefined) {
|
||||
this.defaultDeviceName = conf.DeviceTypeName
|
||||
this.defaultFormData = conf
|
||||
}
|
||||
// 获取point信息
|
||||
const devicePointAddress = []
|
||||
if (conf.RequestNode !== undefined && conf.RequestNode.length > 0) {
|
||||
this.devicePointData = each(conf.RequestNode, (o, i) => {
|
||||
let type
|
||||
if (o['@DataTypeCode'][0] === 'u') {
|
||||
type = split(o['@DataTypeCode'], '')
|
||||
type[1] = toUpper(type[0] + type[1])
|
||||
type = type.join('')
|
||||
} else {
|
||||
type = capitalize(o['@DataTypeCode'])
|
||||
}
|
||||
devicePointAddress.push({
|
||||
key: i,
|
||||
value: o['@Address'],
|
||||
type:
|
||||
o['@DataTypeCode'][0] === 'u'
|
||||
? replace(o['@DataTypeCode'])
|
||||
: capitalize(o['@DataTypeCode'])
|
||||
})
|
||||
assign(o, {
|
||||
id: i + 1,
|
||||
showBindButton: true,
|
||||
showCopyButton: true,
|
||||
showRemoveButton: true
|
||||
})
|
||||
})
|
||||
if (devicePointAddress.length > 0) {
|
||||
this.getDevicePoint(devicePointAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
addDevice () {
|
||||
this.$prompt('输入设备名称', '新加设备', {
|
||||
confirmButtonText: '确定',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入设备名称'
|
||||
}).then(async ({ value }) => {
|
||||
await this.$api.ADD_DEVICE({
|
||||
action: 'add_device',
|
||||
name: value,
|
||||
server_id: this.serverData.id
|
||||
})
|
||||
this.getDevice()
|
||||
this.$message({
|
||||
message: '新加设备成功',
|
||||
type: 'success'
|
||||
})
|
||||
})
|
||||
},
|
||||
async delDevice () {
|
||||
const deviceConfigure = await this.$api.GET_HSLSERVER_CONFIGURE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port
|
||||
)
|
||||
let deviceNode = deviceConfigure.Content.Settings.GroupNode[0].DeviceNode
|
||||
if (deviceNode !== undefined && isArray(deviceNode)) {
|
||||
deviceNode = filter(deviceNode, (item) => {
|
||||
return item['@Name'] === this.deviceData[this.deviceActiveStatus].name
|
||||
? null
|
||||
: item
|
||||
})
|
||||
deviceConfigure.Content.Settings.GroupNode[0].DeviceNode = deviceNode
|
||||
} else {
|
||||
unset(deviceConfigure, 'Content.Settings.GroupNode[0].DeviceNode')
|
||||
}
|
||||
try {
|
||||
await this.$api.SET_HSLSERVER_CONFIGURE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port,
|
||||
{ data: deviceConfigure.Content }
|
||||
)
|
||||
await this.$api.DEL_DEVICE({
|
||||
action: 'remove_device',
|
||||
id: this.deviceData[this.deviceActiveStatus].id
|
||||
})
|
||||
this.$message({
|
||||
message: '删除设备成功',
|
||||
type: 'success'
|
||||
})
|
||||
this.getDevice()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async addDevicePoint (row, done) {
|
||||
this.formOptions.saveLoading = true
|
||||
if (!this.$refs.deviceConfigure.defaultDeviceTypeNameValue) {
|
||||
this.$message({
|
||||
message: '请先应用设备配置',
|
||||
type: 'error'
|
||||
})
|
||||
done(false)
|
||||
} else {
|
||||
if (row['@DataTypeCode'] === 'string' && !row['@Length']) {
|
||||
this.$message({
|
||||
message: '数据类型为string,长度不能为空',
|
||||
type: 'error'
|
||||
})
|
||||
this.formOptions.saveLoading = false
|
||||
return false
|
||||
}
|
||||
assign(row, {
|
||||
id: this.devicePointData.length + 1,
|
||||
showBindButton: true,
|
||||
showCopyButton: true,
|
||||
showRemoveButton: true
|
||||
})
|
||||
this.devicePointData.push(row)
|
||||
done()
|
||||
}
|
||||
this.formOptions.saveLoading = false
|
||||
},
|
||||
async delDevicePoint ({ index, row }, done) {
|
||||
this.$delete(this.devicePointData, index)
|
||||
done()
|
||||
},
|
||||
getDevicePoint (devicePointAddress) {
|
||||
clearInterval(this.timing) // 先关闭上一个定时器
|
||||
this.timing = setInterval(() => {
|
||||
each(devicePointAddress, async (item) => {
|
||||
this.$api
|
||||
.GET_DEVICE_POINT_VALUE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port,
|
||||
this.deviceData[this.deviceActiveStatus].name,
|
||||
item.value,
|
||||
item.type
|
||||
)
|
||||
.then((res) => {
|
||||
this.$set(
|
||||
this.devicePointData[item.key],
|
||||
'@Value',
|
||||
res.Content
|
||||
)
|
||||
})
|
||||
})
|
||||
}, 10000)
|
||||
},
|
||||
async devicePointBandingNode ({ index, row }, done) {
|
||||
this.$delete(this.devicePointData[index], '@Binding')
|
||||
this.$set(this.devicePointData[index], '@Binding', row.nodeCode)
|
||||
done()
|
||||
},
|
||||
async copyDevicePoint ({ row }) {
|
||||
this.$prompt('复制该点位前,请输入设备点位名称', '复制点位', {
|
||||
confirmButtonText: '确定',
|
||||
inputPattern: /^[\s\S]*.*[^\s][\s\S]*$/,
|
||||
inputErrorMessage: '请输入设备点位名称'
|
||||
}).then(async ({ value }) => {
|
||||
row.id = this.devicePointData.length + 1
|
||||
row['@Name'] = value
|
||||
this.devicePointData.push(row)
|
||||
})
|
||||
},
|
||||
setDeviceConfigureConfirm () {
|
||||
this.$confirm('是否要应用该配置信息,更改配置后需要重启改服务才能使配置生效?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
this.setDeviceConfigure()
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已取消应用设备配置'
|
||||
})
|
||||
})
|
||||
},
|
||||
async setDeviceConfigure () {
|
||||
try {
|
||||
// 验证表单
|
||||
this.$refs.deviceConfigure.$refs.form.validate((valid) => {
|
||||
if (!valid) {
|
||||
return false
|
||||
}
|
||||
})
|
||||
const deviceConfigure = await this.$api.GET_HSLSERVER_CONFIGURE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port
|
||||
)
|
||||
const deviceConfigureModelValue = this.$refs.deviceConfigure.deviceConfigureModelValue
|
||||
if (this.devicePointData.length > 0) {
|
||||
let devicePointData = this.devicePointData
|
||||
devicePointData = each(devicePointData, (item) => {
|
||||
if (item['@DataTypeCode'] !== 'string') {
|
||||
unset(item, '@Length')
|
||||
}
|
||||
unset(item, 'id')
|
||||
unset(item, 'showBindButton')
|
||||
unset(item, 'showCopyButton')
|
||||
unset(item, 'showRemoveButton')
|
||||
unset(item, '@Value')
|
||||
item['@Binding'] = item['@Binding'] || ''
|
||||
})
|
||||
deviceConfigureModelValue.RequestNode = devicePointData
|
||||
}
|
||||
deviceConfigureModelValue['@Name'] = this.deviceData[this.deviceActiveStatus].name
|
||||
let deviceNode = deviceConfigure.Content.Settings.GroupNode[0].DeviceNode || []
|
||||
let isExist = false
|
||||
if (deviceNode !== undefined) {
|
||||
deviceNode = isArray(deviceNode) ? deviceNode : [deviceNode]
|
||||
deviceNode = map(deviceNode, (item) => {
|
||||
if (
|
||||
item['@Name'] === this.deviceData[this.deviceActiveStatus].name
|
||||
) {
|
||||
isExist = true
|
||||
item = deviceConfigureModelValue
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
if (!isExist) {
|
||||
deviceNode.push(deviceConfigureModelValue)
|
||||
}
|
||||
deviceConfigure.Content.Settings.GroupNode[0].DeviceNode = deviceNode
|
||||
await this.$api.SET_HSLSERVER_CONFIGURE(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port,
|
||||
{ data: deviceConfigure.Content }
|
||||
)
|
||||
deviceConfigureModelValue.DeviceTypeName =
|
||||
this.$refs.deviceConfigure.defaultDeviceTypeNameValue
|
||||
const data = {
|
||||
action: 'update_device',
|
||||
conf: JSON.stringify(deviceConfigureModelValue),
|
||||
id: this.deviceData[this.deviceActiveStatus].id
|
||||
}
|
||||
this.$api.SET_DEVICE_CONFIGURE(data)
|
||||
this.$message({
|
||||
message: '操作成功',
|
||||
type: 'success'
|
||||
})
|
||||
this.$emit('changeServerConfig', 't')
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async getDeviceStatus () {
|
||||
const res = await this.$api.GET_SERVER_DEVICE_STATUS(
|
||||
'http://' + this.serverData.url + ':' + this.serverData.port
|
||||
)
|
||||
if (res) {
|
||||
this.$emit('changeStatus', 'online')
|
||||
const deviceStatus = {}
|
||||
each(res.Content.__deviceList, (item) => {
|
||||
deviceStatus[item.__name] = {
|
||||
status: item.__requestEnable,
|
||||
name: item.__name
|
||||
}
|
||||
})
|
||||
each(this.deviceData, (item) => {
|
||||
if (deviceStatus[item.name]) {
|
||||
item.status = deviceStatus[item.name].status
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.$emit('changeStatus', false)
|
||||
}
|
||||
},
|
||||
exportDeviceConfigure () {
|
||||
const deviceConfigure = this.$refs.deviceConfigure.deviceConfigureModelValue
|
||||
const devicePointData = this.devicePointData
|
||||
deviceConfigure.DeviceNode = devicePointData
|
||||
deviceConfigure.DeviceTypeName = this.$refs.deviceConfigure.defaultDeviceTypeNameValue
|
||||
const deviceData = JSON.stringify(deviceConfigure)
|
||||
const exportLink = document.createElement('a')
|
||||
const blob = new Blob([deviceData], { type: 'text/json' })
|
||||
exportLink.href = URL.createObjectURL(blob)
|
||||
exportLink.download = deviceConfigure['@Name'] + '.json'
|
||||
exportLink.style.display = 'none'
|
||||
document.body.appendChild(exportLink)
|
||||
exportLink.click()
|
||||
exportLink.remove()
|
||||
},
|
||||
importDeviceConfigure (file) {
|
||||
const reader = new FileReader()
|
||||
reader.readAsText(file, 'UTF-8')
|
||||
const that = this
|
||||
reader.onload = function (e) {
|
||||
const fileData = e.target.result
|
||||
const deviceConfigureData = JSON.parse(fileData)
|
||||
that.devicePointData = deviceConfigureData.DeviceNode
|
||||
unset(deviceConfigureData, 'DeviceNode')
|
||||
that.defaultFormData = deviceConfigureData
|
||||
that.defaultDeviceName = deviceConfigureData.DeviceTypeName
|
||||
}
|
||||
return false
|
||||
},
|
||||
clearTime () {
|
||||
clearInterval(this.timing)
|
||||
this.timing = null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getworkingSubclasses()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.menu-box {
|
||||
box-sizing: border-box;
|
||||
padding: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.el-dropdown {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.el-dropdown+.el-dropdown {
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.el-icon-arrow-down {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.menu-header {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
width: 60%;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: black;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.header-button {
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.serve-item {
|
||||
border: none;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
||||
overflow: hidden;
|
||||
outline: 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.device-status {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
|
||||
right: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
}
|
||||
|
||||
.device-status-online {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
.device-status-offline {
|
||||
background-color: #ccc;
|
||||
}
|
||||
</style>
|
||||
155
src/views/edgeServer/edgeServerConfigure/deviceConfigure.vue
Normal file
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :inline="true" size="mini" :model="deviceConfigureModelValue" ref="form" :rules="rules"
|
||||
label-position="right" class="demo-form-inline">
|
||||
<el-form-item label="设备">
|
||||
<el-select @change="deviceChange" filterable v-model="defaultDeviceTypeNameValue" placeholder="请选择">
|
||||
<el-option v-for="item in deviceTypeData" :key="item" :label="item" :value="item">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<!-- v-if="choosable === 'true'" -->
|
||||
<el-form-item label="设备编码" >
|
||||
<el-select filterable allow-create v-model="deviceConfigureModelValue['@ParentDeviceCode']" placeholder="请选择">
|
||||
<el-option v-for="(item, i) in deviceCodeData" :key="i" :label="item.name" :value="item.code">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<template v-for="(item, i) in deviceConfigureFormItem">
|
||||
<el-form-item :key="i" :label="item.labelName" :prop="item.key" v-if="!item.isShow">
|
||||
<template v-if="item.type === 'text'">
|
||||
<el-input v-model='deviceConfigureModelValue[item.key]' :disabled="item.disabled"
|
||||
:placeholder="item.placeholder"></el-input>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'number'">
|
||||
<el-input-number v-model='deviceConfigureModelValue[item.key]' :disabled="item.disabled" :controls="false"
|
||||
:placeholder="item.placeholder"></el-input-number>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'select'">
|
||||
<el-select v-model='deviceConfigureModelValue[item.key]' style="width:80px" :disabled="item.disabled"
|
||||
:placeholder="item.placeholder">
|
||||
<el-option v-for="index in item.option" :key="index.value" :label="index.label" :value="index.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<template v-if="item.type === 'time'">
|
||||
<el-date-picker v-model='deviceConfigureModelValue[item.key]' type="datetime"
|
||||
:placeholder="item.placeholder" value-format='yyyy-MM-dd HH:mm:ss' format='yyyy-MM-dd HH:mm:ss'
|
||||
:disabled="item.disabled" style="width:180px">
|
||||
</el-date-picker>
|
||||
</template>
|
||||
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import deviceConfigureFormItemData from './deviceSetting/index.json'
|
||||
import { each, assign } from 'lodash'
|
||||
// import { getDeviceAll } from '@/api/basic/device'
|
||||
|
||||
export default {
|
||||
name: 'deviceConfigure',
|
||||
props: {
|
||||
loading: {
|
||||
default: false
|
||||
},
|
||||
defaultDeviceName: {
|
||||
default: ''
|
||||
},
|
||||
defaultFormData: {
|
||||
default: () => []
|
||||
}
|
||||
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
watch: {
|
||||
defaultDeviceName: {
|
||||
handler (val) {
|
||||
this.defaultDeviceTypeNameValue = val
|
||||
this.deviceChange(val)
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
defaultFormData: {
|
||||
handler (val) {
|
||||
this.setDeviceDefaultFormItemValue(val)
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
deviceConfigureFormItem: [],
|
||||
defaultDeviceTypeNameValue: '',
|
||||
deviceCode: '',
|
||||
deviceCodeData: [],
|
||||
choosable: process.env.VUE_APP_CHOOSABLE,
|
||||
deviceConfigureModelValue: {},
|
||||
rules: {},
|
||||
deviceSelectedVlaue: '',
|
||||
deviceTypeData: [
|
||||
]
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deviceChange (e) {
|
||||
// 获取所有字符串的key 用于v-model渲染
|
||||
const deviceConfigureModelValue = {}
|
||||
const rules = {}
|
||||
each(deviceConfigureFormItemData[e], (item) => {
|
||||
if (item.type === 'time') {
|
||||
deviceConfigureModelValue[item.key] = item.defaultValue ? item.defaultValue : new Date()
|
||||
} else {
|
||||
deviceConfigureModelValue[item.key] = item.defaultValue ? item.defaultValue : ''
|
||||
}
|
||||
if (item.rules !== undefined) {
|
||||
rules[item.key] = item.rules
|
||||
}
|
||||
})
|
||||
this.rules = rules
|
||||
this.deviceConfigureModelValue = assign(deviceConfigureModelValue, { '@ParentDeviceCode': '' })
|
||||
// this.deviceConfigureModelValue = this.choosable === 'true' ? assign(deviceConfigureModelValue, { parent_device_code: '' }) : deviceConfigureModelValue
|
||||
this.deviceConfigureFormItem = deviceConfigureFormItemData[e]
|
||||
},
|
||||
setDeviceDefaultFormItemValue (val) {
|
||||
const deviceConfigureModelValue = {}
|
||||
// if (this.choosable === 'true') {
|
||||
// deviceConfigureModelValue["@ParentDeviceCode"] = val["@ParentDeviceCode"]
|
||||
// }
|
||||
deviceConfigureModelValue['@ParentDeviceCode'] = val['@ParentDeviceCode'] || ''
|
||||
each(deviceConfigureFormItemData[this.defaultDeviceTypeNameValue], (item) => {
|
||||
if (item.type === 'time') {
|
||||
deviceConfigureModelValue[item.key] = val[item.key] ? val[item.key] : new Date()
|
||||
} else {
|
||||
deviceConfigureModelValue[item.key] = val[item.key] ? val[item.key] : item.defaultValue
|
||||
}
|
||||
}
|
||||
)
|
||||
this.deviceConfigureModelValue = deviceConfigureModelValue
|
||||
},
|
||||
getDeviceType () {
|
||||
this.deviceTypeData = Object.keys(deviceConfigureFormItemData)
|
||||
},
|
||||
getDeviceAll () {
|
||||
// getDeviceAll().then(res => {
|
||||
// this.deviceCodeData = res.data
|
||||
// })
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.choosable === 'true') {
|
||||
this.getDeviceAll()
|
||||
}
|
||||
this.getDeviceType()
|
||||
}
|
||||
|
||||
}
|
||||
</script>
|
||||
2928
src/views/edgeServer/edgeServerConfigure/deviceSetting/index.json
Normal file
395
src/views/edgeServer/edgeServerConfigure/index.vue
Normal file
@@ -0,0 +1,395 @@
|
||||
<template>
|
||||
<d2-container>
|
||||
<d2-crud
|
||||
ref="d2Crud"
|
||||
:columns="columns"
|
||||
:data="data"
|
||||
:rowHandle="rowHandle"
|
||||
add-title="新增服务"
|
||||
edit-title="修改服务配置"
|
||||
:add-template="addTemplate"
|
||||
:edit-template="editTemplate"
|
||||
:form-options="formOptions"
|
||||
:add-rules="addRules"
|
||||
@dialog-open="handleDialogOpen"
|
||||
@row-add="handleRowAdd"
|
||||
@row-edit="handleRowEdit"
|
||||
@row-remove="handleRowRemove"
|
||||
@dialog-cancel="handleDialogCancel"
|
||||
@set-device="drawerShow"
|
||||
@exec="setServerExec">
|
||||
<el-button slot="header" style="margin-bottom: 5px" @click="addRow">新增</el-button>
|
||||
</d2-crud>
|
||||
|
||||
<el-drawer
|
||||
size="95%"
|
||||
:append-to-body='true'
|
||||
:visible.sync="isDrawerShow"
|
||||
@close="closeDrawer"
|
||||
>
|
||||
<div slot="title" >
|
||||
<span class="title-text">前置服务 {{selectedServerData.name}} URL:{{selectedServerData.url}}</span>
|
||||
<el-tag
|
||||
:color="serveStatus[selectedServerData.status].backgroundColor"
|
||||
:style="{color:serveStatus[selectedServerData.status].textColor,borderColor:serveStatus[selectedServerData.status].borderColor}"
|
||||
>
|
||||
{{serveStatus[selectedServerData.status].name}}
|
||||
</el-tag>
|
||||
<el-alert v-if="this.selectedServerData.updated === 't'" title="设备配置已更改,请在服务配置界面重启对应服务生效" type="warning" style="width:400px;display: inline-block;vertical-align: middle; margin-left: 5px;" :closable="false" />
|
||||
</div>
|
||||
<device ref="deviceTemplate" :server='server' @changeStatus='changeSelectedServerStatus' @changeServerConfig='changeSelectedServerConfig' />
|
||||
</el-drawer>
|
||||
|
||||
</d2-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { assign, each } from 'lodash'
|
||||
|
||||
const genRanHex = size => [...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16).toUpperCase()).join('')
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Device: () => import('./device')
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
columns: [
|
||||
{
|
||||
title: '序号',
|
||||
key: 'id'
|
||||
},
|
||||
{
|
||||
title: '名称',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
key: 'url'
|
||||
},
|
||||
{
|
||||
title: '端口',
|
||||
key: 'port'
|
||||
},
|
||||
{
|
||||
title: '绑定地址',
|
||||
key: 'address'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'create_date'
|
||||
},
|
||||
{
|
||||
title: '备注',
|
||||
key: 'note'
|
||||
}
|
||||
],
|
||||
isDrawerShow: false,
|
||||
server: {},
|
||||
serveStatus: {
|
||||
online: {
|
||||
name: '在线',
|
||||
textColor: '#67c23a',
|
||||
backgroundColor: '#f0f9eb',
|
||||
borderColor: '#e1f3d8'
|
||||
},
|
||||
offline: {
|
||||
name: '离线',
|
||||
textColor: '#67c23a',
|
||||
backgroundColor: '#f0f9eb',
|
||||
borderColor: '#e1f3d8'
|
||||
}
|
||||
},
|
||||
selectedServerData: {
|
||||
name: '',
|
||||
url: '',
|
||||
port: '',
|
||||
updated: 'f',
|
||||
status: 'offline'
|
||||
},
|
||||
data: [],
|
||||
serverSettings: {},
|
||||
rowHandle: {
|
||||
minWidth: '200',
|
||||
edit: {
|
||||
icon: 'el-icon-edit',
|
||||
text: '编辑',
|
||||
size: 'small',
|
||||
show (index, row) {
|
||||
if (row.showEditButton) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
remove: {
|
||||
icon: 'el-icon-delete',
|
||||
size: 'small',
|
||||
fixed: 'right',
|
||||
confirm: true,
|
||||
show (index, row) {
|
||||
if (row.showRemoveButton) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
},
|
||||
custom: [
|
||||
{
|
||||
text: '设备配置',
|
||||
size: 'small',
|
||||
emit: 'set-device'
|
||||
},
|
||||
{
|
||||
text: '重启服务',
|
||||
size: 'small',
|
||||
emit: 'exec'
|
||||
}
|
||||
]
|
||||
},
|
||||
addTemplate: {
|
||||
deviceName: {
|
||||
title: '服务名称',
|
||||
value: 'Edge:' + genRanHex(12)
|
||||
},
|
||||
uniqueID: {
|
||||
title: '唯一标识',
|
||||
value: '',
|
||||
component: {
|
||||
disabled: true,
|
||||
placeholder: '采集服务唯一标识,测试成功后返回'
|
||||
}
|
||||
},
|
||||
url: {
|
||||
title: 'URL',
|
||||
value: '',
|
||||
component: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
port: {
|
||||
title: '端口',
|
||||
value: 522,
|
||||
component: {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
address: {
|
||||
title: '绑定地址',
|
||||
value: '',
|
||||
component: {
|
||||
placeholder: '请填入绑定EdgeManager实例的地址,比如http://xxx.xxx:xxxx'
|
||||
}
|
||||
}
|
||||
},
|
||||
editTemplate: {
|
||||
address: {
|
||||
title: '绑定地址',
|
||||
component: {
|
||||
placeholder: '仅可以修改已绑定EdgeManager实例的地址'
|
||||
}
|
||||
}
|
||||
},
|
||||
formOptions: {
|
||||
labelWidth: '80px',
|
||||
labelPosition: 'left',
|
||||
saveLoading: false,
|
||||
saveButtonText: '测试',
|
||||
saveButtonType: 'text'
|
||||
},
|
||||
addRules: {
|
||||
deviceName: [{ required: true, type: 'string', message: '服务名称不可为空', trigger: 'blur' }],
|
||||
url: [{ required: true, type: 'string', message: '服务地址不可为空', trigger: 'blur' }],
|
||||
port: [{ required: true, type: 'integer', transform: v => +v, message: '端口号必须指定,且需为(0, 65535]内的正整数', trigger: 'blur' }],
|
||||
userName: [{ required: true, message: '用户名不可为空', trigger: 'blur' }],
|
||||
password: [{ required: true, type: 'string', message: '密码不可为空', trigger: 'blur' }],
|
||||
address: [{ type: 'url', message: '绑定地址必须为合法URL', trigger: 'blur' }]
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async getServers () {
|
||||
try {
|
||||
const res = await this.$api.QUERY_SERVERS()
|
||||
this.data = each(res, (o) => (
|
||||
assign(o, {
|
||||
showEditButton: true,
|
||||
showRemoveButton: true
|
||||
})
|
||||
))
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
handleDialogOpen ({ mode }) {
|
||||
if (mode === 'edit') {
|
||||
this.formOptions.saveButtonText = '确定'
|
||||
this.formOptions.saveButtonType = 'text'
|
||||
}
|
||||
},
|
||||
// 普通的新增
|
||||
addRow () {
|
||||
this.$refs.d2Crud.showDialog({
|
||||
mode: 'add'
|
||||
})
|
||||
},
|
||||
async handleRowAdd (row, done) {
|
||||
this.formOptions.saveLoading = true
|
||||
try {
|
||||
// 定义测试动作
|
||||
if (this.formOptions.saveButtonText === '测试') {
|
||||
this.serverSettings = await this.$api.VERIFY_SERVER(
|
||||
'http://' + row.url + ':' + row.port
|
||||
)
|
||||
if (this.serverSettings) {
|
||||
this.$message({
|
||||
message: '测试通过!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
each(Object.keys(row), (p) => {
|
||||
this.addTemplate[p].value = row[p]
|
||||
})
|
||||
this.addTemplate.uniqueID.value = this.serverSettings.Content.ServerInfoConfig.UniqueId
|
||||
this.$refs.d2Crud.showDialog({
|
||||
mode: 'add'
|
||||
})
|
||||
this.formOptions.saveButtonText = '添加'
|
||||
this.formOptions.saveButtonType = 'success'
|
||||
// 定义添加动作
|
||||
} else {
|
||||
this.serverSettings.Content.ServerInfoConfig.DeviceName = row.deviceName
|
||||
this.serverSettings.Content.ServerInfoConfig.CaptureURL = row.address
|
||||
this.$api.MODIFY_SERVER(
|
||||
'http://' + row.url + ':' + row.port,
|
||||
{ data: this.serverSettings.Content }
|
||||
)
|
||||
await this.$api.ADD_SERVER({
|
||||
action: 'add_server',
|
||||
name: row.deviceName,
|
||||
url: row.url,
|
||||
port: row.port,
|
||||
address: row.address
|
||||
})
|
||||
this.$message({
|
||||
message: '添加成功',
|
||||
type: 'success'
|
||||
})
|
||||
this.getServers()
|
||||
done()
|
||||
}
|
||||
} catch (e) {
|
||||
this.$message({
|
||||
message: '测试/添加失败!',
|
||||
type: 'error'
|
||||
})
|
||||
console.log(e)
|
||||
}
|
||||
this.formOptions.saveLoading = false
|
||||
},
|
||||
async handleRowEdit ({ index, row }, done) {
|
||||
this.formOptions.saveLoading = true
|
||||
const serverSettings = await this.$api.VERIFY_SERVER(
|
||||
'http://' + row.url + ':' + row.port
|
||||
)
|
||||
serverSettings.Content.ServerInfoConfig.CaptureURL = row.address
|
||||
this.$api.MODIFY_SERVER(
|
||||
'http://' + row.url + ':' + row.port,
|
||||
{ data: serverSettings.Content }
|
||||
)
|
||||
this.$api.UPDATE_SERVER({
|
||||
action: 'update_server',
|
||||
id: row.id,
|
||||
address: row.address
|
||||
})
|
||||
this.$message({
|
||||
message: '编辑成功',
|
||||
type: 'success'
|
||||
})
|
||||
done()
|
||||
this.formOptions.saveLoading = false
|
||||
},
|
||||
async handleRowRemove ({ index, row }, done) {
|
||||
await this.$api.REMOVE_SERVER({
|
||||
action: 'remove_server',
|
||||
id: row.id
|
||||
})
|
||||
this.$message({
|
||||
message: '删除成功',
|
||||
type: 'success'
|
||||
})
|
||||
done()
|
||||
},
|
||||
setServerExec ({ row }) {
|
||||
this.$confirm('是否要重启动该服务', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async () => {
|
||||
try {
|
||||
await this.$api.SET_SERVER_EXEC({
|
||||
action: 'exec',
|
||||
server_id: row.id,
|
||||
command: 'server_restart'
|
||||
})
|
||||
this.$message({
|
||||
message: '服务应用请求成功,请求动作已添加至请求队列中,请从服务监控页面查看结果',
|
||||
type: 'success'
|
||||
})
|
||||
this.getServers()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
}).catch(() => {
|
||||
this.$message({
|
||||
type: 'info',
|
||||
message: '已重启动该服务'
|
||||
})
|
||||
})
|
||||
},
|
||||
handleDialogCancel (done) {
|
||||
this.$message({
|
||||
message: '用户放弃改动',
|
||||
type: 'warning'
|
||||
})
|
||||
done()
|
||||
},
|
||||
drawerShow ({ row }) {
|
||||
this.selectedServerData = {
|
||||
name: row.name,
|
||||
url: row.url,
|
||||
port: row.port,
|
||||
updated: row.updated,
|
||||
status: row.status ? row.status : 'offline'
|
||||
}
|
||||
this.server = { id: row.id, url: row.url, port: row.port }
|
||||
this.isDrawerShow = true
|
||||
},
|
||||
closeDrawer () {
|
||||
this.server = { id: 0, url: '', port: '' }
|
||||
if (this.$refs.deviceTemplate.timing) {
|
||||
this.$refs.deviceTemplate.clearTime()
|
||||
}
|
||||
},
|
||||
changeSelectedServerStatus (status) {
|
||||
this.selectedServerData.status = status || 'offline'
|
||||
},
|
||||
changeSelectedServerConfig (updated) {
|
||||
this.getServers()
|
||||
this.selectedServerData.updated = updated || 'f'
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getServers()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.title-text{
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: black;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
</style>
|
||||
252
src/views/edgeServer/edgeServerMonitor/index.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<d2-container>
|
||||
<div>
|
||||
<el-card
|
||||
class="box-card"
|
||||
v-for="(item, index) in serverData"
|
||||
:key="index"
|
||||
>
|
||||
<div slot="header" class="device-header">
|
||||
<span
|
||||
>前置服务 {{ item.server_name }} ({{ item.url }}:{{ item.port }} )
|
||||
<el-tag
|
||||
v-if="item.status !== undefined"
|
||||
:color="serveStatus[item.status].backgroundColor"
|
||||
:style="{color:serveStatus[item.status].textColor,borderColor:serveStatus[item.status].borderColor}"
|
||||
>
|
||||
{{ serveStatus[item.status].name }}
|
||||
</el-tag>
|
||||
</span>
|
||||
</div>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="6" v-for="(i, k) in item.devices" :key="k">
|
||||
<el-popover placement="top" trigger="click">
|
||||
<el-menu
|
||||
@select="(index) => setDeviceExec(i, index, item)"
|
||||
:style="{ borderRight: 'none' }"
|
||||
>
|
||||
<el-menu-item index="device_stop">
|
||||
<span slot="title">暂停</span>
|
||||
</el-menu-item>
|
||||
<el-menu-item index="device_continue">
|
||||
<span slot="title">启动</span>
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
<el-card
|
||||
slot="reference"
|
||||
:class="{
|
||||
'box-card': true,
|
||||
online: i.deviceStatus,
|
||||
offline: !i.deviceStatus,
|
||||
}"
|
||||
>
|
||||
<div slot="header" class="header-title">
|
||||
<span>{{ i.device_name }}</span>
|
||||
</div>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>设备类型</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.device_type }}</el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>IP/端口</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.config }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>活动时间</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.startTime }}</el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>采集耗时</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.captureSpendTime }}</el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>成功次数</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.success }}</el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-col
|
||||
:span="5"
|
||||
style="padding-right: 0px; margin-bottom: 5px"
|
||||
>
|
||||
<span>失败次数</span>
|
||||
</el-col>
|
||||
<el-col :span="14" style="padding-left: 0px"
|
||||
>:{{ i.failed }}</el-col
|
||||
>
|
||||
</el-row>
|
||||
<el-row :gutter="24">
|
||||
<el-alert
|
||||
v-if="i.failedMsg"
|
||||
:title="i.failedMsg"
|
||||
type="error"
|
||||
:closable="false"
|
||||
>
|
||||
</el-alert>
|
||||
<div v-else style="height: 35px"></div>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</d2-container>
|
||||
</template>
|
||||
<script>
|
||||
import { unset, map } from 'lodash'
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
serverData: [],
|
||||
deviceData: [],
|
||||
deviceCommand: {},
|
||||
serveStatus: {
|
||||
online: {
|
||||
name: '在线',
|
||||
textColor: '#67c23a',
|
||||
backgroundColor: '#f0f9eb',
|
||||
borderColor: '#e1f3d8'
|
||||
},
|
||||
offline: {
|
||||
name: '离线',
|
||||
textColor: '#67c23a',
|
||||
backgroundColor: '#f0f9eb',
|
||||
borderColor: '#e1f3d8'
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
async setDeviceExec (device, command, server) {
|
||||
try {
|
||||
await this.$api.SET_SERVER_EXEC({
|
||||
action: 'exec',
|
||||
server_id: server.id,
|
||||
command: command,
|
||||
device_name: device.device_name
|
||||
})
|
||||
this.$message({
|
||||
message: '请求成功,请求动作已添加至请求队列中',
|
||||
type: 'success'
|
||||
})
|
||||
this.getServe()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
},
|
||||
async getServe () {
|
||||
this.serverData = await this.$api.QUERY_SERVERS()
|
||||
this.serverData.forEach((element, index) => {
|
||||
this.$api
|
||||
.GET_SERVE_DEVICE_MONITORING(
|
||||
'http://' + element.url + ':' + element.port
|
||||
)
|
||||
.then((deviceStatus) => {
|
||||
if (deviceStatus.IsSuccess) {
|
||||
const data = deviceStatus.Content
|
||||
unset(data, '__status')
|
||||
const deviceData = map(data, (element) => {
|
||||
const config = element.__config.split(' ')
|
||||
return {
|
||||
config: config.length > 0 ? config[2] : '',
|
||||
startTime: element.__startTime,
|
||||
captureSpendTime: element.__captureSpendTime,
|
||||
success: element.__success,
|
||||
failed: element.__failed,
|
||||
failedMsg: element.__failedMsg,
|
||||
deviceStatus: element.__requestEnable,
|
||||
device_name: element.__name,
|
||||
device_type: config.length > 0 ? config[0] : ''
|
||||
}
|
||||
})
|
||||
this.$set(this.serverData[index], 'devices', deviceData)
|
||||
this.$set(this.serverData[index], 'status', 'online')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.$message({
|
||||
message: this.serverData[index].name + '服务请求失败',
|
||||
type: 'error'
|
||||
})
|
||||
this.$set(this.serverData[index], 'status', 'offline')
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.getServe()
|
||||
setInterval(() => {
|
||||
this.getServe()
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped>
|
||||
.box-card {
|
||||
margin: 5px 0px;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.device-header {
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.online {
|
||||
background-color: #67c23a;
|
||||
}
|
||||
|
||||
.offline {
|
||||
background-color: #666;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.item {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
</style>
|
||||
@@ -42,11 +42,13 @@
|
||||
import tips from './tips.md'
|
||||
import jschardet from 'jschardet'
|
||||
import { assign, each, pick, pickBy, startsWith, includes, every } from 'lodash'
|
||||
// import { getWorkingsubclassAll } from '@/api/basic/device'
|
||||
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
tips,
|
||||
choosable: process.env.VUE_APP_CHOOSABLE,
|
||||
columns: [
|
||||
{
|
||||
title: '序号',
|
||||
@@ -65,17 +67,13 @@ export default {
|
||||
key: 'type'
|
||||
},
|
||||
{
|
||||
title: '流程',
|
||||
key: 'flow_code'
|
||||
title: '数据类别',
|
||||
key: 'category'
|
||||
},
|
||||
{
|
||||
title: '工序单元',
|
||||
key: 'working_subclass'
|
||||
},
|
||||
{
|
||||
title: '工作站',
|
||||
key: 'workstation'
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
key: 'create_date'
|
||||
@@ -145,14 +143,49 @@ export default {
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
flow_code: {
|
||||
title: '流程'
|
||||
category: {
|
||||
title: '数据类别',
|
||||
value: '',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
options: [
|
||||
{
|
||||
value: 'DEVICE_STATUS',
|
||||
label: '设备状态'
|
||||
},
|
||||
{
|
||||
value: 'ITEM_ID_ITEM',
|
||||
label: '编号'
|
||||
},
|
||||
{
|
||||
value: 'DEVICE_DATA',
|
||||
label: '运行数据'
|
||||
},
|
||||
{
|
||||
value: 'PROCESS_DATA',
|
||||
label: '过程数据'
|
||||
},
|
||||
{
|
||||
value: 'RESULT_DATA',
|
||||
label: '结果数据'
|
||||
},
|
||||
{
|
||||
value: 'STATISTICAL_DATA',
|
||||
label: '统计数据'
|
||||
}
|
||||
],
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
working_subclass: {
|
||||
title: '工序单元'
|
||||
},
|
||||
workstation: {
|
||||
title: '工作站'
|
||||
title: '工序单元',
|
||||
component: {
|
||||
name: 'el-select',
|
||||
filterable: true,
|
||||
allowCreate: true,
|
||||
options: this.WorkingsubclassData,
|
||||
span: 12
|
||||
}
|
||||
},
|
||||
note: {
|
||||
title: '备注'
|
||||
@@ -162,12 +195,6 @@ export default {
|
||||
name: {
|
||||
title: '节点名称'
|
||||
},
|
||||
flow_code: {
|
||||
title: '流程'
|
||||
},
|
||||
workstation: {
|
||||
title: '工作站'
|
||||
},
|
||||
note: {
|
||||
title: '备注'
|
||||
}
|
||||
@@ -181,6 +208,7 @@ export default {
|
||||
code: [{ required: true, type: 'string', message: '节点编码必须填写', trigger: 'blur' }],
|
||||
name: [{ required: true, type: 'string', message: '节点名称必须填写', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '必须指定节点类型', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '必须指定数据类别', trigger: 'blur' }],
|
||||
working_subclass: [{
|
||||
required: true,
|
||||
type: 'string',
|
||||
@@ -193,7 +221,7 @@ export default {
|
||||
methods: {
|
||||
async getNodes () {
|
||||
try {
|
||||
const res = await this.$api.QUERY_NODE()
|
||||
const res = await this.$api.QUERY_NODES()
|
||||
this.data = each(res, (o) => (
|
||||
assign(o, {
|
||||
showEditButton: true,
|
||||
@@ -206,6 +234,10 @@ export default {
|
||||
},
|
||||
// 普通的新增
|
||||
addRow () {
|
||||
this.$nextTick(() => {
|
||||
this.$refs.d2Crud.handleFormTemplateMode('working_subclass').component.options = this.WorkingsubclassData
|
||||
this.$refs.d2Crud.$forceUpdate()
|
||||
})
|
||||
this.$refs.d2Crud.showDialog({
|
||||
mode: 'add'
|
||||
})
|
||||
@@ -294,9 +326,19 @@ export default {
|
||||
type: 'warning'
|
||||
})
|
||||
done()
|
||||
},
|
||||
getWorkingsubclassAll () {
|
||||
// getWorkingsubclassAll().then(res=>{
|
||||
// this.WorkingsubclassData = map(res.data, (o) => {
|
||||
// return { value: o.code, label: o.name }
|
||||
// })
|
||||
// })
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.choosable === 'true') {
|
||||
this.getWorkingsubclassAll()
|
||||
}
|
||||
this.getNodes()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
<c-grid
|
||||
:filter="tableFilter"
|
||||
:key='key'
|
||||
:loading="loading"
|
||||
:data="data">
|
||||
<c-grid-column
|
||||
field="id"
|
||||
@@ -209,6 +210,7 @@ export default {
|
||||
time: [now - 3600 * 1000 * 24 * 7, +now],
|
||||
code: ''
|
||||
},
|
||||
loading: false,
|
||||
workingSubclasses: [],
|
||||
codes: [],
|
||||
data: [],
|
||||
@@ -334,6 +336,7 @@ export default {
|
||||
}
|
||||
},
|
||||
async getData () {
|
||||
this.loading = true
|
||||
this.excelData = []
|
||||
if (!this.formInline.workingSubclass) {
|
||||
this.$message.error('请选择工序单元')
|
||||
@@ -348,6 +351,7 @@ export default {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
}
|
||||
this.loading = false
|
||||
},
|
||||
async exportExcel () {
|
||||
const data = this.excelData.length === 0 ? this.data : this.excelData
|
||||
@@ -390,6 +394,7 @@ export default {
|
||||
this.$message.error('请先选择比较方法!')
|
||||
return false
|
||||
}
|
||||
|
||||
this.excelData = []
|
||||
this.key = !this.key // 取反重新组件渲染
|
||||
},
|
||||
|
||||