From 57e5c736c00501cd4b745471b12f0c5bdb044fb4 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Wed, 16 Jul 2025 14:32:09 +0800 Subject: [PATCH] first commit --- README.md | 3 + app/cpcc.py | 60 +++++ conf/config.json | 81 ++++++ docs/cpc数据表.xlsx | Bin 0 -> 9603 bytes json/build.sh | 3 + json/components.json | 13 + json/cpccluster.json | 13 + json/cpclist.json | 58 +++++ json/cpcnode.json | 16 ++ models/components.xlsx | Bin 0 -> 16826 bytes models/cpccluster.xlsx | Bin 0 -> 16981 bytes models/cpclist.xlsx | Bin 0 -> 17068 bytes models/cpcnode.xlsx | Bin 0 -> 17105 bytes models/cpcnode_config.xlsx | Bin 0 -> 16789 bytes models/cpcnode_config_detail.xlsx | Bin 0 -> 16772 bytes models/cpvalue.xlsx | Bin 0 -> 16795 bytes models/mysql.ddl.sql | 201 +++++++++++++++ script/cpcc_roleperm.sh | 3 + wwwroot/app_panel.ui | 40 +++ wwwroot/bottom.ui | 17 ++ wwwroot/center.ui | 15 ++ wwwroot/components/add_components.dspy | 35 +++ wwwroot/components/delete_components.dspy | 33 +++ wwwroot/components/get_components.dspy | 83 ++++++ wwwroot/components/index.ui | 122 +++++++++ wwwroot/components/update_components.dspy | 32 +++ wwwroot/cpccluster/add_cpccluster.dspy | 35 +++ wwwroot/cpccluster/delete_cpccluster.dspy | 33 +++ wwwroot/cpccluster/get_cpccluster.dspy | 91 +++++++ wwwroot/cpccluster/index.ui | 239 ++++++++++++++++++ wwwroot/cpccluster/update_cpccluster.dspy | 32 +++ wwwroot/cpclist/add_cpclist.dspy | 52 ++++ wwwroot/cpclist/delete_cpclist.dspy | 47 ++++ wwwroot/cpclist/get_cpclist.dspy | 123 +++++++++ wwwroot/cpclist/index.ui | 209 ++++++++++++++++ wwwroot/cpclist/update_cpclist.dspy | 49 ++++ wwwroot/cpcnode/add_cpcnode.dspy | 38 +++ wwwroot/cpcnode/delete_cpcnode.dspy | 33 +++ wwwroot/cpcnode/get_cpcnode.dspy | 126 ++++++++++ wwwroot/cpcnode/index.ui | 179 +++++++++++++ wwwroot/cpcnode/update_cpcnode.dspy | 35 +++ wwwroot/cpcpod/get_cpcpod.dspy | 76 ++++++ wwwroot/cpcpod/get_node_labels.dspy | 11 + wwwroot/cpcpod/index.ui | 179 +++++++++++++ wwwroot/cpcpod/new_cpcpodyaml.dspy | 101 ++++++++ wwwroot/cpcpod/new_podyaml.ui | 265 ++++++++++++++++++++ wwwroot/cpcpodyaml/delete_cpcpodyaml.dspy | 138 ++++++++++ wwwroot/cpcpodyaml/get_cpcpodyaml.dspy | 121 +++++++++ wwwroot/cpcpodyaml/index.ui | 278 +++++++++++++++++++++ wwwroot/cpcpodyaml/update_cpcpodyaml.dspy | 110 ++++++++ wwwroot/cpcworker/add_cpcworker.dspy | 35 +++ wwwroot/cpcworker/delete_cpcworker.dspy | 33 +++ wwwroot/cpcworker/get_availableworker.dspy | 11 + wwwroot/cpcworker/get_cpcworker.dspy | 84 +++++++ wwwroot/cpcworker/index.ui | 179 +++++++++++++ wwwroot/cpcworker/new_cpcworker.dspy | 83 ++++++ wwwroot/cpcworker/new_worker.ui | 33 +++ wwwroot/cpcworker/update_cpcworker.dspy | 32 +++ wwwroot/handy/get_cpcnodes.dspy | 12 + wwwroot/handy/new_cluster.dspy | 97 +++++++ wwwroot/handy/new_cluster.ui | 79 ++++++ wwwroot/index.ui | 27 ++ wwwroot/menu.ui | 20 ++ wwwroot/top.ui | 23 ++ 64 files changed, 4176 insertions(+) create mode 100644 README.md create mode 100644 app/cpcc.py create mode 100755 conf/config.json create mode 100644 docs/cpc数据表.xlsx create mode 100755 json/build.sh create mode 100644 json/components.json create mode 100644 json/cpccluster.json create mode 100644 json/cpclist.json create mode 100644 json/cpcnode.json create mode 100644 models/components.xlsx create mode 100644 models/cpccluster.xlsx create mode 100644 models/cpclist.xlsx create mode 100644 models/cpcnode.xlsx create mode 100644 models/cpcnode_config.xlsx create mode 100644 models/cpcnode_config_detail.xlsx create mode 100644 models/cpvalue.xlsx create mode 100644 models/mysql.ddl.sql create mode 100644 script/cpcc_roleperm.sh create mode 100644 wwwroot/app_panel.ui create mode 100644 wwwroot/bottom.ui create mode 100644 wwwroot/center.ui create mode 100644 wwwroot/components/add_components.dspy create mode 100644 wwwroot/components/delete_components.dspy create mode 100644 wwwroot/components/get_components.dspy create mode 100644 wwwroot/components/index.ui create mode 100644 wwwroot/components/update_components.dspy create mode 100644 wwwroot/cpccluster/add_cpccluster.dspy create mode 100644 wwwroot/cpccluster/delete_cpccluster.dspy create mode 100644 wwwroot/cpccluster/get_cpccluster.dspy create mode 100644 wwwroot/cpccluster/index.ui create mode 100644 wwwroot/cpccluster/update_cpccluster.dspy create mode 100644 wwwroot/cpclist/add_cpclist.dspy create mode 100644 wwwroot/cpclist/delete_cpclist.dspy create mode 100644 wwwroot/cpclist/get_cpclist.dspy create mode 100644 wwwroot/cpclist/index.ui create mode 100644 wwwroot/cpclist/update_cpclist.dspy create mode 100644 wwwroot/cpcnode/add_cpcnode.dspy create mode 100644 wwwroot/cpcnode/delete_cpcnode.dspy create mode 100644 wwwroot/cpcnode/get_cpcnode.dspy create mode 100644 wwwroot/cpcnode/index.ui create mode 100644 wwwroot/cpcnode/update_cpcnode.dspy create mode 100644 wwwroot/cpcpod/get_cpcpod.dspy create mode 100644 wwwroot/cpcpod/get_node_labels.dspy create mode 100644 wwwroot/cpcpod/index.ui create mode 100644 wwwroot/cpcpod/new_cpcpodyaml.dspy create mode 100644 wwwroot/cpcpod/new_podyaml.ui create mode 100644 wwwroot/cpcpodyaml/delete_cpcpodyaml.dspy create mode 100644 wwwroot/cpcpodyaml/get_cpcpodyaml.dspy create mode 100644 wwwroot/cpcpodyaml/index.ui create mode 100644 wwwroot/cpcpodyaml/update_cpcpodyaml.dspy create mode 100644 wwwroot/cpcworker/add_cpcworker.dspy create mode 100644 wwwroot/cpcworker/delete_cpcworker.dspy create mode 100644 wwwroot/cpcworker/get_availableworker.dspy create mode 100644 wwwroot/cpcworker/get_cpcworker.dspy create mode 100644 wwwroot/cpcworker/index.ui create mode 100644 wwwroot/cpcworker/new_cpcworker.dspy create mode 100644 wwwroot/cpcworker/new_worker.ui create mode 100644 wwwroot/cpcworker/update_cpcworker.dspy create mode 100644 wwwroot/handy/get_cpcnodes.dspy create mode 100644 wwwroot/handy/new_cluster.dspy create mode 100644 wwwroot/handy/new_cluster.ui create mode 100644 wwwroot/index.ui create mode 100644 wwwroot/menu.ui create mode 100644 wwwroot/top.ui diff --git a/README.md b/README.md new file mode 100644 index 0000000..44db15f --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# cpcc + +a computing power center manages client \ No newline at end of file diff --git a/app/cpcc.py b/app/cpcc.py new file mode 100644 index 0000000..f395fe2 --- /dev/null +++ b/app/cpcc.py @@ -0,0 +1,60 @@ +from appPublic.registerfunction import RegisterFunction +from ahserver.webapp import webapp +from ahserver.serverenv import ServerEnv +from appbase.init import load_appbase +from rbac.check_perm import load_rbac + +def UiError(title="出错", message="出错啦", timeout=5): + return { + "widgettype":"Error", + "options":{ + "author":"tr", + "timeout":timeout, + "cwidth":15, + "cheight":10, + "title":title, + "message":message + } + } + +def UiMessage(title="消息", message="后台消息", timeout=5): + return { + "widgettype":"Message", + "options":{ + "author":"tr", + "timeout":timeout, + "cwidth":15, + "cheight":10, + "title":title, + "message":message + } + } + +def get_module_dbname(mname): + return 'cpcc' + +rf = RegisterFunction() +rf.register('get_module_dbname', get_module_dbname) + +def init(): + g = ServerEnv() + g.get_module_dbname = get_module_dbname + load_appbase() + load_rbac() + g.UiError = UiError + g.UiMessage = UiMessage + + # k8sAPI远程指令(目前放到.dspy文件中) + # 看样子cpcc.py文件中没有使用到数据库函数 + # g.cluster_kubeconfig = cluster_kubeconfig + + g.result_dict = { + "status": False, + "info": "failed", + "data": None + } + +if __name__ == '__main__': + webapp(init) + +# sword/111111 \ No newline at end of file diff --git a/conf/config.json b/conf/config.json new file mode 100755 index 0000000..f9b6977 --- /dev/null +++ b/conf/config.json @@ -0,0 +1,81 @@ +{ + "password_key":"!@#$%^&*(*&^%$QWERTYUIqwertyui234567", + "logger":{ + "name":"cpcc", + "levelname":"clientinfo", + "logfile":"$[workdir]$/logs/cpcc.log" + }, + "filesroot":"$[workdir]$/files", + "databases":{ + "cpcc":{ + "driver":"aiomysql", + "async_mode":true, + "coding":"utf8", + "maxconn":100, + "dbname":"cpcc", + "kwargs":{ + "user":"test", + "db":"cpcc", + "password":"QUZVcXg5V1p1STMybG5Ia6mX9D0v7+g=", + "host":"localhost" + } + } + }, + "website":{ + "paths":[ + ["$[workdir]$/wwwroot",""] + ], + "client_max_size":10000, + "host":"0.0.0.0", + "port":9203, + "coding":"utf-8", + "ssl_gg":{ + "crtfile":"$[workdir]$/conf/www.bsppo.com.pem", + "keyfile":"$[workdir]$/conf/www.bsppo.com.key" + }, + "indexes":[ + "index.html", + "index.tmpl", + "index.ui", + "index.dspy", + "index.md" + ], + "startswiths":[ + { + "leading":"/idfile", + "registerfunction":"idFileDownload" + } + ], + "processors":[ + [".ws","ws"], + [".xterm","xterm"], + [".proxy","proxy"], + [".llm", "llm"], + [".llms", "llms"], + [".llma", "llma"], + [".xlsxds","xlsxds"], + [".sqlds","sqlds"], + [".tmpl.js","tmpl"], + [".tmpl.css","tmpl"], + [".html.tmpl","tmpl"], + [".bcrud", "bricks_crud"], + [".tmpl","tmpl"], + [".app","app"], + [".bui","bui"], + [".ui","bui"], + [".dspy","dspy"], + [".md","md"] + ], + "session_max_time":3000, + "session_issue_time":2500, + "session_redis_rrrr":{ + "url":"redis://127.0.0.1:6379" + } + }, + "langMapping":{ + "zh-Hans-CN":"zh-cn", + "zh-CN":"zh-cn", + "en-us":"en", + "en-US":"en" + } +} diff --git a/docs/cpc数据表.xlsx b/docs/cpc数据表.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..20b0d9bba3298fb35aa58162c1787e398270b88f GIT binary patch literal 9603 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 literal 0 HcmV?d00001 diff --git a/json/build.sh b/json/build.sh new file mode 100755 index 0000000..aae3368 --- /dev/null +++ b/json/build.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +xls2ui -m ../models -o ../wwwroot cpcc *.json diff --git a/json/components.json b/json/components.json new file mode 100644 index 0000000..d32b941 --- /dev/null +++ b/json/components.json @@ -0,0 +1,13 @@ +{ + "tblname":"components", + "params":{ + "browserfields": { + "exclouded": ["id"], + "alters": {} + }, + "editexclouded": [ + "id" + ] + } +} + diff --git a/json/cpccluster.json b/json/cpccluster.json new file mode 100644 index 0000000..7a82651 --- /dev/null +++ b/json/cpccluster.json @@ -0,0 +1,13 @@ +{ + "tblname":"cpccluster", + "params":{ + "browserfields": { + "exclouded": ["id", "cpcid"], + "alters": {} + }, + "editexclouded": [ + "id", "cpcid" + ] + } +} + diff --git a/json/cpclist.json b/json/cpclist.json new file mode 100644 index 0000000..b1685f9 --- /dev/null +++ b/json/cpclist.json @@ -0,0 +1,58 @@ +{ + "tblname":"cpclist", + "params":{ + "toolbar":{ + "tools":[ + { + "name":"newcluster", + "selected_row":true, + "label":"新建集群" + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"newcluster", + "actiontype":"urlwidget", + "target":"PopupWindow", + "popup_options":{ + "width":"80%", + "height":"80%" + }, + "options":{ + "url":"{{entire_url('../handy/new_cluster.ui')}}", + "params":{ + "id":"{{params_kw.id}}" + } + } + } + ], + "logined_userorgid":"orgid", + "confidential_fields":[ + "api_pwd" + ], + "browserfields": { + "exclouded": [ + "id","orgid", "pcapi_url","pcapi_user", "pcapi_pwd" + ], + "alters": {} + }, + "editexclouded": [ + "id", "orgid" + ], + "subtables":[ + { + "subtable":"cpccluster", + "field":"cpcid", + "title":"集群" + }, + { + "subtable":"cpcnode", + "field":"cpcid", + "title":"节点" + } + ] + } +} + diff --git a/json/cpcnode.json b/json/cpcnode.json new file mode 100644 index 0000000..2658de0 --- /dev/null +++ b/json/cpcnode.json @@ -0,0 +1,16 @@ +{ + "tblname":"cpcnode", + "params":{ + "confidential_fields":["adminpwd"], + "browserfields": { + "exclouded": [ + "id", "cpcid" + ], + "alters": {} + }, + "editexclouded": [ + "id", "cpcid", "clusterid" + ] + } +} + diff --git a/models/components.xlsx b/models/components.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..644b1000cd85e5d9b9f981a6037e9889bcc52c9f GIT binary patch literal 16826 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 literal 0 HcmV?d00001 diff --git a/models/cpccluster.xlsx b/models/cpccluster.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..22cae5ec99d234bb708f3a00d0cb11916db8ef66 GIT binary patch literal 16981 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 literal 0 HcmV?d00001 diff --git a/models/cpcnode_config.xlsx b/models/cpcnode_config.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..a7a8c472658bfbb61bf9e91bcf0838c886202be3 GIT binary patch literal 16789 zcmeHuWmH^U(k=v-;2PZBU4pw?Lx2$6-8DdPcXxMpC%8*+cMYE4dOMk!n|GLZ?z;cK z?=$cnL?9+*lAoeLK|rX0m#84nZ#0FitsD%k z9JEzjZ4B+T=v^!=2y?*SP-cO=0gnIQq1x9JFvd|nNEP=XZGXd)nQbfB@$4F*Aw$&{wQJ>`SK1YF)lL;chKoN_eIb@y zbufdStVrU3PtQioVpRp1nMpl9Hz$FBew2KCtY(!^OA}Qyj%|3sdX?vPTQ}}vmcJ@$P8
73!3d}UOV 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 literal 0 HcmV?d00001 diff --git a/models/cpcnode_config_detail.xlsx b/models/cpcnode_config_detail.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..b16d3ad3e5c727f8a91718731ecd630657a31659 GIT binary patch literal 16772 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 literal 0 HcmV?d00001 diff --git a/models/mysql.ddl.sql b/models/mysql.ddl.sql new file mode 100644 index 0000000..780c9b4 --- /dev/null +++ b/models/mysql.ddl.sql @@ -0,0 +1,201 @@ + +-- ./cpcnode_config.xlsx + + + + + +drop table if exists cpcnode_config; +CREATE TABLE cpcnode_config +( + + `id` VARCHAR(32) comment 'id', + `nodeid` VARCHAR(32) comment '节点名称', + `name` VARCHAR(255) comment '名称', + `description` VARCHAR(3000) comment '描述' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '节点配置表' +; + + +-- ./cpccluster.xlsx + + + + + +drop table if exists cpccluster; +CREATE TABLE cpccluster +( + + `id` VARCHAR(32) comment 'id', + `name` VARCHAR(255) comment '名称', + `cpcid` VARCHAR(32) NOT NULL comment '算力中心id', + `clustertype` VARCHAR(1) comment '集群类型', + `controllerid` VARCHAR(32) comment '控制节点id', + `enable_date` date comment '启用日期', + `export_date` date comment '停用日期' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '算力集群' +; + + +-- ./cpcnode.xlsx + + + + + +drop table if exists cpcnode; +CREATE TABLE cpcnode +( + + `id` VARCHAR(32) comment 'id', + `name` VARCHAR(255) comment '名称', + `ip` VARCHAR(90) comment '内网ip', + `sshport` int comment 'ssh端口号', + `adminuser` VARCHAR(99) comment '管理账号', + `adminpwd` VARCHAR(99) comment '管理密码', + `cpcid` VARCHAR(32) NOT NULL comment '算力中心id', + `node_status` VARCHAR(1) comment '节点状态', + `clusterid` int comment '所属集群', + `enable_date` date comment '启用日期', + `export_date` date comment '停用日期' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '算力节点' +; + + +-- ./cpvalue.xlsx + + + + + +drop table if exists cpvalue; +CREATE TABLE cpvalue +( + + `id` VARCHAR(32) comment 'id', + `comid` VARCHAR(255) comment '部件id', + `cpval` double(18,3) comment '算力值', + `cpunit` VARCHAR(1) comment '算力单位' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '算力值' +; + + +-- ./cpclist.xlsx + + + + + +drop table if exists cpclist; +CREATE TABLE cpclist +( + + `id` VARCHAR(32) comment 'id', + `name` VARCHAR(255) comment '名称', + `orgid` VARCHAR(32) comment '属主机构id', + `pcapi_url` VARCHAR(500) comment 'pcapi网址', + `api_user` VARCHAR(100) comment '接口用户', + `api_pwd` VARCHAR(100) comment '接口密码', + `enable_date` date comment '启用日期', + `expire_date` date comment '停用日期', + `contactname` VARCHAR(100) comment '联系人', + `contactphone` VARCHAR(100) comment '联系电话' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '算力中心列表' +; + + +-- ./components.xlsx + + + + + +drop table if exists components; +CREATE TABLE components +( + + `id` VARCHAR(32) comment 'id', + `name` VARCHAR(255) comment '名称', + `ccatelogid` VARCHAR(255) comment '部件分类', + `cmodel` VARCHAR(255) comment '型号', + `unitname` VARCHAR(32) comment '计量名', + `unitvalue` int comment '计量值' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '部件表' +; + + +-- ./cpcnode_config_detail.xlsx + + + + + +drop table if exists cpcnode_config_detail; +CREATE TABLE cpcnode_config_detail +( + + `id` VARCHAR(32) comment 'id', + `nodeconfigid` VARCHAR(32) comment '节点配置id', + `comid` VARCHAR(32) comment '部件id', + `comcnt` int comment '部件数量' + + +,primary key(id) + + +) +engine=innodb +default charset=utf8 +comment '节点配置明细项' +; + + diff --git a/script/cpcc_roleperm.sh b/script/cpcc_roleperm.sh new file mode 100644 index 0000000..94bc48e --- /dev/null +++ b/script/cpcc_roleperm.sh @@ -0,0 +1,3 @@ +#!/usr/bin/bash + +python ~/py/rbac/script/roleperm.py sage cpcc reseller operator cpclist cpcnode cpccluster components diff --git a/wwwroot/app_panel.ui b/wwwroot/app_panel.ui new file mode 100644 index 0000000..98a920f --- /dev/null +++ b/wwwroot/app_panel.ui @@ -0,0 +1,40 @@ +{ + "widgettype":"HBox", + "options":{ + "css":"filler" + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "text":"主菜单", + "tip":"进入主菜单", + "css":"clickable" + }, + "binds":[ + { + "wid":"self", + "event":"click", + "actiontype":"urlwidget", + "target":"Popup", + "popup_options":{ + "eventpos":true, + "dismiss_events":["command"] + }, + "options":{ + "url":"{{entire_url('menu.ui')}}" + } + } + ] + }, + { + "widgettype":"Title4", + "options":{ + "i18n":true, + "otext":"大模型应用平台", + "wrap":true, + "halign":"left" + } + } + ] +} diff --git a/wwwroot/bottom.ui b/wwwroot/bottom.ui new file mode 100644 index 0000000..0158542 --- /dev/null +++ b/wwwroot/bottom.ui @@ -0,0 +1,17 @@ +{ + "widgettype":"HBox", + "options":{ + "cheight":2, + "bgcolor":"#e5e5e5" + }, + "subwidgets":[ + { + "widgettype":"Text", + "options":{ + "otext":"© 2024 版权所有, 开元云(北京)科技有限公司", + "i18n":true, + "wrap":true + } + } + ] +} diff --git a/wwwroot/center.ui b/wwwroot/center.ui new file mode 100644 index 0000000..d5cdc5d --- /dev/null +++ b/wwwroot/center.ui @@ -0,0 +1,15 @@ +{ + "widgettype":"VBox", + "id":"page_center", + "options":{ + "css":"filler" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('/appbase/appcodes')}}" + } + } + ] +} diff --git a/wwwroot/components/add_components.dspy b/wwwroot/components/add_components.dspy new file mode 100644 index 0000000..88e19b6 --- /dev/null +++ b/wwwroot/components/add_components.dspy @@ -0,0 +1,35 @@ + +ns = params_kw.copy() +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('components', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/components/delete_components.dspy b/wwwroot/components/delete_components.dspy new file mode 100644 index 0000000..8e18ccf --- /dev/null +++ b/wwwroot/components/delete_components.dspy @@ -0,0 +1,33 @@ + +ns = { + 'id':params_kw['id'], +} + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('components', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/components/get_components.dspy b/wwwroot/components/get_components.dspy new file mode 100644 index 0000000..98e93a1 --- /dev/null +++ b/wwwroot/components/get_components.dspy @@ -0,0 +1,83 @@ + +ns = params_kw.copy() + + +debug(f'get_components.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.ccatelogid_text +from (select * from components where 1=1 [[filterstr]]) a left join (select k as ccatelogid, + v as ccatelogid_text from appcodes_kv where parentid='ccatelog') b on a.ccatelogid = b.ccatelogid''' + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255 + }, + { + "name": "ccatelogid", + "title": "部件分类", + "type": "str", + "length": 255 + }, + { + "name": "cmodel", + "title": "型号", + "type": "str", + "length": 255 + }, + { + "name": "unitname", + "title": "计量名", + "type": "str", + "length": 32 + }, + { + "name": "unitvalue", + "title": "计量值", + "type": "short" + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/components/index.ui b/wwwroot/components/index.ui new file mode 100644 index 0000000..4d874a1 --- /dev/null +++ b/wwwroot/components/index.ui @@ -0,0 +1,122 @@ + +{ + "id":"components_tbl", + "widgettype":"Tabular", + "options":{ + + + "title":"部件表", + + + + + "css":"card", + + "editable":{ + "new_data_url":"{{entire_url('add_components.dspy')}}", + "delete_data_url":"{{entire_url('delete_components.dspy')}}", + "update_data_url":"{{entire_url('update_components.dspy')}}" + }, + + + "data_url":"{{entire_url('./get_components.dspy')}}", + "data_method":"GET", + "data_params":{{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options":{ + + + + "browserfields": { + "exclouded": [ + "id" + ], + "alters": {} +}, + + + "editexclouded":[ + "id" +], + + "fields":[ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "名称" + }, + { + "name": "ccatelogid", + "title": "部件分类", + "type": "str", + "length": 255, + "label": "部件分类", + "uitype": "code", + "valueField": "ccatelogid", + "textField": "ccatelogid_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "ccatelogid", + "textField": "ccatelogid_text", + "cond": "parentid='ccatelog'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "cmodel", + "title": "型号", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "型号" + }, + { + "name": "unitname", + "title": "计量名", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "计量名" + }, + { + "name": "unitvalue", + "title": "计量值", + "type": "short", + "length": 0, + "uitype": "int", + "datatype": "short", + "label": "计量值" + } +] + }, + + + + "page_rows":160, + "cache_limit":5 + } + + ,"binds":[] + +} \ No newline at end of file diff --git a/wwwroot/components/update_components.dspy b/wwwroot/components/update_components.dspy new file mode 100644 index 0000000..6362a54 --- /dev/null +++ b/wwwroot/components/update_components.dspy @@ -0,0 +1,32 @@ + +ns = params_kw.copy() + + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('components', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpccluster/add_cpccluster.dspy b/wwwroot/cpccluster/add_cpccluster.dspy new file mode 100644 index 0000000..361a2ba --- /dev/null +++ b/wwwroot/cpccluster/add_cpccluster.dspy @@ -0,0 +1,35 @@ + +ns = params_kw.copy() +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('cpccluster', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpccluster/delete_cpccluster.dspy b/wwwroot/cpccluster/delete_cpccluster.dspy new file mode 100644 index 0000000..3d40fe8 --- /dev/null +++ b/wwwroot/cpccluster/delete_cpccluster.dspy @@ -0,0 +1,33 @@ + +ns = { + 'id':params_kw['id'], +} + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('cpccluster', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok,记得把关联的节点都解除占用状态 ~" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpccluster/get_cpccluster.dspy b/wwwroot/cpccluster/get_cpccluster.dspy new file mode 100644 index 0000000..c69433b --- /dev/null +++ b/wwwroot/cpccluster/get_cpccluster.dspy @@ -0,0 +1,91 @@ + +ns = params_kw.copy() + + +debug(f'get_cpccluster.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + +sql = '''select a.*, b.clustertype_text, c.controllerid_text +from (select * from cpccluster where 1=1 [[filterstr]]) a left join (select k as clustertype, + v as clustertype_text from appcodes_kv where parentid='clustertype') b on a.clustertype = b.clustertype left join (select id as controllerid, + ip as controllerid_text from cpcnode where 1 = 1) c on a.controllerid = c.controllerid''' + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255 + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "clustertype", + "title": "集群类型", + "type": "str", + "length": 1 + }, + { + "name": "controllerid", + "title": "控制节点id", + "type": "str", + "length": 32 + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 255 + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 255 + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/cpccluster/index.ui b/wwwroot/cpccluster/index.ui new file mode 100644 index 0000000..1725fd3 --- /dev/null +++ b/wwwroot/cpccluster/index.ui @@ -0,0 +1,239 @@ +{ + "id": "cpccluster_tbl", + "widgettype": "Tabular", + "options": { + "title": "算力集群", + "css": "card", + "toolbar": { + "tools": [ + { + "name": "newworker", + "selected_row": true, + "label": "新增工作节点" + }, + { + "name": "newpodyaml", + "selected_row": true, + "label": "新建资源YAML" + } + ], + "css": "float-right" + }, + "editable": { + "new_data_url": "{{entire_url('add_cpccluster.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpccluster.dspy')}}", + "update_data_url": "{{entire_url('update_cpccluster.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpccluster.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "id", + "cpcid", + "clusterid" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "cpcid", + "clusterid" + ], + "fields": [ + { + "name": "name", + "title": "集群名称", + "type": "str", + "length": 255, + "cwidth": 14, + "uitype": "str", + "datatype": "str", + "label": "集群名称" + }, + { + "name": "controllerid", + "title": "控制节点IP", + "type": "str", + "length": 32, + "cwidth": 12, + "label": "控制节点IP", + "uitype": "code", + "valueField": "controllerid", + "textField": "controllerid_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "cpcnode", + "tblvalue": "id", + "tbltext": "name", + "valueField": "controllerid", + "textField": "controllerid_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "算力中心id" + }, + { + "name": "clustertype", + "title": "集群类型", + "type": "str", + "length": 1, + "cwidth": 8, + "label": "集群类型", + "uitype": "code", + "valueField": "clustertype", + "textField": "clustertype_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "clustertype", + "textField": "clustertype_text", + "cond": "parentid='clustertype'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "clusterjoin", + "title": "加入集群凭证", + "type": "str", + "length": 255, + "cwidth": 45, + "uitype": "str", + "datatype": "str", + "label": "加入集群凭证" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 0, + "cwidth": 8, + "uitype": "date", + "datatype": "date", + "label": "启用日期" + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 0, + "cwidth": 8, + "uitype": "date", + "datatype": "date", + "label": "停用日期" + } + ] + }, + "content_view": { + "widgettype": "TabPanel", + "options": { + "tab_wide": "auto", + "height": "100%", + "width": "100%", + "tab_pos": "top", + "items": [ + { + "name": "cpcpod", + "label": "实时资源实例", + "content": { + "widgettype": "urlwidget", + "options": { + "params": { + "clusterid": "${id}", + "cpcid": "${cpcid}" + }, + "url": "{{entire_url('..\/cpcpod')}}" + } + } + }, + { + "name": "cpcworker", + "label": "实时集群节点", + "content": { + "widgettype": "urlwidget", + "options": { + "params": { + "clusterid": "${id}", + "cpcid": "${cpcid}" + }, + "url": "{{entire_url('..\/cpcworker')}}" + } + } + }, + { + "name": "cpcyamlconfig", + "label": "资源实例配置", + "content": { + "widgettype": "urlwidget", + "options": { + "params": { + "clusterid": "${id}", + "cpcid": "${cpcid}" + }, + "url": "{{entire_url('..\/cpcpodyaml')}}" + } + } + } + ] + } + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [ + { + "wid": "self", + "event": "newworker", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "width": "80%", + "height": "80%" + }, + "options": { + "url": "{{entire_url('../cpcworker/new_worker.ui')}}", + "params": { + "id": "{{params_kw.id}}" + } + } + }, + { + "wid": "self", + "event": "newpodyaml", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "width": "80%", + "height": "80%" + }, + "options": { + "url": "{{entire_url('../cpcpod/new_podyaml.ui')}}", + "params": { + "id": "{{params_kw.id}}" + } + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/cpccluster/update_cpccluster.dspy b/wwwroot/cpccluster/update_cpccluster.dspy new file mode 100644 index 0000000..af390dd --- /dev/null +++ b/wwwroot/cpccluster/update_cpccluster.dspy @@ -0,0 +1,32 @@ + +ns = params_kw.copy() + + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('cpccluster', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpclist/add_cpclist.dspy b/wwwroot/cpclist/add_cpclist.dspy new file mode 100644 index 0000000..55b2d24 --- /dev/null +++ b/wwwroot/cpclist/add_cpclist.dspy @@ -0,0 +1,52 @@ + +ns = params_kw.copy() +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + +if params_kw.get('api_pwd'): + ns['api_pwd'] = password_encode(params_kw.get('api_pwd')) + + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('cpclist', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpclist/delete_cpclist.dspy b/wwwroot/cpclist/delete_cpclist.dspy new file mode 100644 index 0000000..f9ab4b6 --- /dev/null +++ b/wwwroot/cpclist/delete_cpclist.dspy @@ -0,0 +1,47 @@ + +ns = { + 'id':params_kw['id'], +} + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('cpclist', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpclist/get_cpclist.dspy b/wwwroot/cpclist/get_cpclist.dspy new file mode 100644 index 0000000..f6bd8ea --- /dev/null +++ b/wwwroot/cpclist/get_cpclist.dspy @@ -0,0 +1,123 @@ + +ns = params_kw.copy() + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid +ns['userorgid'] = userorgid + +debug(f'get_cpclist.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.orgid_text +from (select * from cpclist where 1=1 [[filterstr]]) a left join (select id as orgid, + orgname as orgid_text from organization where 1 = 1) b on a.orgid = b.orgid''' + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255 + }, + { + "name": "orgid", + "title": "属主机构id", + "type": "str", + "length": 32 + }, + { + "name": "pcapi_url", + "title": "pcapi网址", + "type": "str", + "length": 500 + }, + { + "name": "api_user", + "title": "接口用户", + "type": "str", + "length": 100 + }, + { + "name": "api_pwd", + "title": "接口密码", + "type": "str", + "length": 100 + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 255 + }, + { + "name": "expire_date", + "title": "停用日期", + "type": "date", + "length": 255 + }, + { + "name": "contactname", + "title": "联系人", + "type": "str", + "length": 100 + }, + { + "name": "contactphone", + "title": "联系电话", + "type": "str", + "length": 100 + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/cpclist/index.ui b/wwwroot/cpclist/index.ui new file mode 100644 index 0000000..a6edc72 --- /dev/null +++ b/wwwroot/cpclist/index.ui @@ -0,0 +1,209 @@ +{ + "id": "cpclist_tbl", + "widgettype": "Tabular", + "options": { + "title": "算力中心列表", + "toolbar": { + "tools": [ + { + "name": "newcluster", + "selected_row": true, + "label": "新建集群" + } + ] + }, + "css": "card", + "editable": { + "new_data_url": "{{entire_url('add_cpclist.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpclist.dspy')}}", + "update_data_url": "{{entire_url('update_cpclist.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpclist.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "id", + "orgid", + "pcapi_url", + "pcapi_user", + "pcapi_pwd" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "orgid" + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "名称" + }, + { + "name": "orgid", + "title": "属主机构id", + "type": "str", + "length": 32, + "label": "属主机构id", + "uitype": "code", + "valueField": "orgid", + "textField": "orgid_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "organization", + "tblvalue": "id", + "tbltext": "orgname", + "valueField": "orgid", + "textField": "orgid_text" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "pcapi_url", + "title": "pcapi网址", + "type": "str", + "length": 500, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "pcapi网址" + }, + { + "name": "api_user", + "title": "接口用户", + "type": "str", + "length": 100, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "接口用户" + }, + { + "name": "api_pwd", + "title": "接口密码", + "type": "str", + "length": 100, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "接口密码" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "启用日期" + }, + { + "name": "expire_date", + "title": "停用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "停用日期" + }, + { + "name": "contactname", + "title": "联系人", + "type": "str", + "length": 100, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "联系人" + }, + { + "name": "contactphone", + "title": "联系电话", + "type": "str", + "length": 100, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "联系电话" + } + ] + }, + "content_view": { + "widgettype": "TabPanel", + "options": { + "tab_wide": "auto", + "height": "100%", + "width": "100%", + "tab_pos": "top", + "items": [ + { + "name": "cpccluster", + "label": "集群", + "content": { + "widgettype": "urlwidget", + "options": { + "params": { + "cpcid": "${id}" + }, + "url": "{{entire_url('..\/cpccluster')}}" + } + } + }, + { + "name": "cpcnode", + "label": "节点", + "content": { + "widgettype": "urlwidget", + "options": { + "params": { + "cpcid": "${id}" + }, + "url": "{{entire_url('..\/cpcnode')}}" + } + } + } + ] + } + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [ + { + "wid": "self", + "event": "newcluster", + "actiontype": "urlwidget", + "target": "PopupWindow", + "popup_options": { + "width": "80%", + "height": "80%" + }, + "options": { + "url": "{{entire_url('../handy/new_cluster.ui')}}", + "params": { + "id": "{{params_kw.id}}" + } + } + } + ] +} \ No newline at end of file diff --git a/wwwroot/cpclist/update_cpclist.dspy b/wwwroot/cpclist/update_cpclist.dspy new file mode 100644 index 0000000..8325779 --- /dev/null +++ b/wwwroot/cpclist/update_cpclist.dspy @@ -0,0 +1,49 @@ + +ns = params_kw.copy() + + +userorgid = await get_userorgid() +if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } +ns['orgid'] = userorgid + + +if params_kw.get('api_pwd'): + ns['api_pwd'] = password_encode(params_kw.get('api_pwd')) + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('cpclist', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcnode/add_cpcnode.dspy b/wwwroot/cpcnode/add_cpcnode.dspy new file mode 100644 index 0000000..3493aa7 --- /dev/null +++ b/wwwroot/cpcnode/add_cpcnode.dspy @@ -0,0 +1,38 @@ + +ns = params_kw.copy() +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + +if params_kw.get('adminpwd'): + ns['adminpwd'] = password_encode(params_kw.get('adminpwd')) + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('cpcnode', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcnode/delete_cpcnode.dspy b/wwwroot/cpcnode/delete_cpcnode.dspy new file mode 100644 index 0000000..a9d4f04 --- /dev/null +++ b/wwwroot/cpcnode/delete_cpcnode.dspy @@ -0,0 +1,33 @@ + +ns = { + 'id':params_kw['id'], +} + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('cpcnode', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcnode/get_cpcnode.dspy b/wwwroot/cpcnode/get_cpcnode.dspy new file mode 100644 index 0000000..e6c5f5a --- /dev/null +++ b/wwwroot/cpcnode/get_cpcnode.dspy @@ -0,0 +1,126 @@ + +ns = params_kw.copy() + + +debug(f'get_cpcnode.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select a.*, b.node_status_text,d.devicetype_text +from (select * from cpcnode where 1=1 [[filterstr]]) +a left join (select k as node_status, v as node_status_text from appcodes_kv where parentid='node_status') +b on a.node_status = b.node_status +left join (select k as devicetype, v as devicetype_text from appcodes_kv where parentid = 'devicetype') +d on a.devicetype = d.devicetype''' + + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255 + }, + { + "name": "devicetype", + "title": "设备类型", + "type": "str", + "length": 1 + }, + { + "name": "ip", + "title": "内网ip", + "type": "str", + "length": 90 + }, + { + "name": "sshport", + "title": "ssh端口号", + "type": "short" + }, + { + "name": "adminuser", + "title": "管理账号", + "type": "str", + "length": 99 + }, + { + "name": "adminpwd", + "title": "管理密码", + "type": "str", + "length": 99 + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "node_status", + "title": "节点状态", + "type": "str", + "length": 1, + "nullable": "yes" + }, + { + "name": "clusterid", + "title": "所属集群", + "type": "long", + "length": 32, + "nullable": "yes" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 255 + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 255 + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/cpcnode/index.ui b/wwwroot/cpcnode/index.ui new file mode 100644 index 0000000..c689cd3 --- /dev/null +++ b/wwwroot/cpcnode/index.ui @@ -0,0 +1,179 @@ +{ + "id": "cpcnode_tbl", + "widgettype": "Tabular", + "options": { + "title": "入网算力节点", + "css": "card", + "editable": { + "new_data_url": "{{entire_url('add_cpcnode.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpcnode.dspy')}}", + "update_data_url": "{{entire_url('update_cpcnode.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpcnode.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "id", + "cpcid", + "clusterid" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "cpcid", + "clusterid" + ], + "fields": [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "ip", + "title": "内网ip", + "type": "str", + "length": 90, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "内网ip" + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "名称" + }, + { + "name": "devicetype", + "title": "设备类型", + "type": "str", + "length": 1, + "cwidth": 8, + "label": "设备类型", + "uitype": "code", + "valueField": "devicetype", + "textField": "devicetype_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "devicetype", + "textField": "devicetype_text", + "cond": "parentid='devicetype'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "sshport", + "title": "ssh端口号", + "type": "short", + "length": 0, + "uitype": "int", + "datatype": "short", + "label": "ssh端口号" + }, + { + "name": "adminuser", + "title": "管理账号", + "type": "str", + "length": 99, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "管理账号" + }, + { + "name": "adminpwd", + "title": "管理密码", + "type": "str", + "length": 99, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "管理密码" + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "算力中心id" + }, + { + "name": "node_status", + "title": "节点状态", + "type": "str", + "length": 1, + "nullable": "yes", + "label": "节点状态", + "uitype": "code", + "valueField": "node_status", + "textField": "node_status_text", + "params": { + "dbname": "{{rfexe('get_module_dbname', 'cpcc')}}", + "table": "appcodes_kv", + "tblvalue": "k", + "tbltext": "v", + "valueField": "node_status", + "textField": "node_status_text", + "cond": "parentid='node_status'" + }, + "dataurl": "{{entire_url('/appbase/get_code.dspy')}}" + }, + { + "name": "clusterid", + "title": "所属集群", + "type": "long", + "length": 0, + "nullable": "yes", + "cwidth": 18, + "uitype": "int", + "datatype": "long", + "label": "所属集群" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "启用日期" + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "停用日期" + } + ] + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [] +} \ No newline at end of file diff --git a/wwwroot/cpcnode/update_cpcnode.dspy b/wwwroot/cpcnode/update_cpcnode.dspy new file mode 100644 index 0000000..137ddcf --- /dev/null +++ b/wwwroot/cpcnode/update_cpcnode.dspy @@ -0,0 +1,35 @@ + +ns = params_kw.copy() + + + +if params_kw.get('adminpwd'): + ns['adminpwd'] = password_encode(params_kw.get('adminpwd')) + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('cpcnode', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcpod/get_cpcpod.dspy b/wwwroot/cpcpod/get_cpcpod.dspy new file mode 100644 index 0000000..428a421 --- /dev/null +++ b/wwwroot/cpcpod/get_cpcpod.dspy @@ -0,0 +1,76 @@ +ns = params_kw.copy() + +debug(f'get_cpcworker.dspy:{ns=}') +#{'_webbricks_': '1', 'width': '1139', 'height': '940', '_is_mobile': '0', +#'clusterid': '4hBm8atruISOU2bs24t_N', 'cpcid': 'AROU9udKtPNyh0AZtO_WY', 'page': '1', 'rows': '160'} + +cpcid = params_kw.cpcid +clusterid = params_kw.clusterid + +# 写数据库的步骤先pass +dbname = get_module_dbname('cpcc') +db = DBPools() + +async with db.sqlorContext(dbname) as sor: + # 通过算力中心ID,获取算力中心部署所在设备的http(s)协议信息 + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + # 通过集群ID,获取算力集群控制节点所在设备的ssh协议参数 + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + nodeid = cpcluster.get("controllerid") + kubeconfig = cpcluster.get("kubeconfig") + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/get_cluster_pods" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + + hc = HttpClient() + + import requests + params = { + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'kubeconfig':kubeconfig + } + debug(f'请求参数{params=}') + + #resp = await hc.request(url, method='GET', + # headers = headers, + # data=params + #) + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + #debug(f'{type(resp)=}->{resp=}') + debug(f"pcapi返回值: {json.loads(resp)=}") + data = json.loads(resp).get('data') + if json.loads(resp).get("status") == True: + return data + else: + return UiError(title='get cluster nodes', message='failed') + +return UiError(title='get cluster nodes', message='uncatched error') \ No newline at end of file diff --git a/wwwroot/cpcpod/get_node_labels.dspy b/wwwroot/cpcpod/get_node_labels.dspy new file mode 100644 index 0000000..baca0d7 --- /dev/null +++ b/wwwroot/cpcpod/get_node_labels.dspy @@ -0,0 +1,11 @@ +db = DBPools() +dbname = get_module_dbname('cpcc') +ns = { + "clusterid":params_kw.clusterid +} +async with db.sqlorContext(dbname) as sor: + sql = "select cpcid,id,ip,node_status from cpcnode where cpcid = (select cpcid from cpccluster where id = ${clusterid}$) and node_status = '0';" + recs = await sor.sqlExe(sql, ns.copy()) + return recs +exception(f'{sql=},{ns=}') +return [] \ No newline at end of file diff --git a/wwwroot/cpcpod/index.ui b/wwwroot/cpcpod/index.ui new file mode 100644 index 0000000..ed00628 --- /dev/null +++ b/wwwroot/cpcpod/index.ui @@ -0,0 +1,179 @@ +{ + "id": "cpcpod_tbl", + "widgettype": "Tabular", + "options": { + "title": "集群实时资源实例", + "description":"显示非系统命名空间下实时资源实例", + "css": "card", + "editable": { + "new_data_url": "{{entire_url('add_cpcpod.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpcpod.dspy')}}", + "update_data_url": "{{entire_url('update_cpcpod.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpcpod.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "id", + "cpcid", + "clusterid" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "cpcid", + "clusterid" + ], + "fields": [ + { + "name": "pod_namespace", + "title": "命名空间", + "type": "str", + "length": 255, + "cwidth": 14, + "uitype": "str", + "datatype": "str", + "label": "命名空间" + }, + { + "name": "pod_name", + "title": "资源实例名称", + "type": "str", + "length": 255, + "cwidth": 20, + "uitype": "str", + "datatype": "str", + "label": "资源实例名称" + }, + { + "name": "pod_ready", + "title": "就绪状态", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "就绪状态" + }, + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "算力中心id" + }, + { + "name": "pod_running", + "title": "运行状态", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 10 , + "uitype": "str", + "datatype": "str", + "label": "运行状态" + }, + { + "name": "pod_restart", + "title": "重启次数", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "重启次数" + }, + { + "name": "pod_age", + "title": "运行时长", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "运行时长" + }, + { + "name": "pod_ip", + "title": "集群内部IP", + "type": "str", + "length": 255, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "集群内部IP" + }, + { + "name": "pod_node", + "title": "Pod节点主机名", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "Pod节点主机名" + }, + { + "name": "pod_nominated_node", + "title": "Pod指定节点", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "Pod指定节点" + }, + { + "name": "pod_cpurate", + "title": "cpu占用率", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "cpu占用率" + }, + { + "name": "pod_memrate", + "title": "内存占用率", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "内存占用率" + }, + { + "name": "pod_readiness_gates", + "title": "盖茨准备", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "盖茨准备" + } + ] + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [] +} \ No newline at end of file diff --git a/wwwroot/cpcpod/new_cpcpodyaml.dspy b/wwwroot/cpcpod/new_cpcpodyaml.dspy new file mode 100644 index 0000000..9a6302c --- /dev/null +++ b/wwwroot/cpcpod/new_cpcpodyaml.dspy @@ -0,0 +1,101 @@ +# params_kw: +# clusterid +debug(f'Web参数:{params_kw=}') +dbname = get_module_dbname('cpcc') +db = DBPools() +clusterid = params_kw.clusterid + +async with db.sqlorContext(dbname) as sor: + # clusterid -> nodeid + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + # 此处的nodeid即控制节点id + kubeconfig = cpcluster.get("kubeconfig") + nodeid = cpcluster.get("controllerid") + cpcid = cpcluster.get("cpcid") + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/yaml_apply" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + userorgid = await get_userorgid() + debug(f'当前组织ID:{userorgid}') + if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + ns_name = clusterid.replace("_","-") + "-" + userorgid # 集群信息+组织信息合成命名空间 + ns_name = ns_name.lower() + hc = HttpClient() + # type参数:更新和新增都是为apply,删除为delete + yamlconfig_id = uuid().replace("_","-").lower() + import requests + params = { + 'cluster_type': "k8s", + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'kubeconfig':kubeconfig, + 'action':'apply', + 'namespace_name': ns_name, + 'serviceaccount_name': ns_name + "-serviceaccount", + 'podcd_name': yamlconfig_id + "-" + params_kw.get("source_podengine").lower(), + 'service_name': yamlconfig_id + "-service", + 'instance_type': params_kw.get("instance_type") #目前仅支持RelationalDB和LinuxOS + } + params.update(params_kw) + + keys_to_remove = ['_webbricks_', 'width', 'height', '_is_mobile'] + for key in keys_to_remove: + if key in params: + del params[key] + + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + if resp.status_code != 200: + return UiError(title='新增YAML参数', message=f'算力中心服务异常 {resp.status_code}') + debug(f'{type(resp)=}->{resp=}') + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + keys_to_remove = ['cluster_type', 'host', 'port', 'user', 'psssword', 'kubeconfig'] + for key in keys_to_remove: + if key in params: + del params[key] + params['id'] = yamlconfig_id + params['cpcid'] = cpcid + if json.loads(resp).get("status") == True: + debug(f"更新资源yaml配置元数据: {params=}") + await sor.C('yaml_config', params) + return UiMessage(title='新增YAML参数', message='资源实例参数更新成功,请10秒后查看实时资源实例面板') + else: + return UiError(title='新增YAML参数', message='资源实例数据写入数据库失败') + +return UiError(title='新增YAML参数', message='全局未知异常') diff --git a/wwwroot/cpcpod/new_podyaml.ui b/wwwroot/cpcpod/new_podyaml.ui new file mode 100644 index 0000000..9674295 --- /dev/null +++ b/wwwroot/cpcpod/new_podyaml.ui @@ -0,0 +1,265 @@ +{ + "widgettype":"Form", + "options":{ + "title":"新建资源YAML模板", + "description":"通过实例化资源YAML,为算力中心的kubernetes集群新增资源实例(注意:kubernetes部分参数值只允许小写,若拼接请以'-'间隔)", + "fields":[ + { + "name":"clusterid", + "value":"{{params_kw.id}}", + "uitype":"hide" + }, + { + "name": "source_name", + "title": "资源容器名称", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源容器名称(可以-分隔)" + }, + { + "name": "source_authuser", + "title": "资源初始账号", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源初始账号" + }, + { + "name": "source_authpasswd", + "title": "资源初始密码", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源初始密码" + }, + { + "name": "source_podengine", + "label":"资源实例控制器", + "data":[ + { + "value":"Deployment", + "text":"Deployment" + }, + { + "value":"Job", + "text":"Job" + }, + { + "value":"CronJob", + "text":"CronJob" + }, + { + "value":"DaemonSet", + "text":"DaemonSet" + }, + { + "value":"StatefulSet", + "text":"StatefulSet" + }, + { + "value":"standalone", + "text":"standalone" + } + ], + "value":"StatefulSet", + "uitype":"code" + }, + { + "name": "source_replicasetnum", + "title": "资源副本个数", + "type": "short", + "length": 0, + "uitype": "int", + "cwidth": 18, + "datatype": "str", + "label": "资源副本个数" + }, + { + "name":"instance_type", + "label":"资源实例类型", + "data":[ + { + "value":"RelationalDB", + "text":"关系型数据库" + }, + { + "value":"LinuxOS", + "text":"Linux操作系统" + } + ], + "value":"LinuxOS", + "uitype":"code" + }, + { + "name":"pod_imagepath", + "label":"基础镜像", + "data":[ + { + "value":"docker.io/library/mysql:8.0", + "text":"docker.io/library/mysql:8.0" + }, + { + "value":"docker.io/library/nginx:latest", + "text":"docker.io/library/nginx:latest" + }, + { + "value":"docker.io/library/ubuntu:22.04", + "text":"docker.io/library/ubuntu:22.04" + }, + { + "value":"docker.io/jupyter/base-notebook:latest", + "text":"docker.io/jupyter/base-notebook:latest" + } + ], + "value":"docker.io/library/ubuntu:22.04", + "uitype":"code" + }, + { + "name": "source_memrate", + "title": "资源内存限制", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源内存限制(示例:100Mi)" + }, + { + "name": "source_cpurate", + "title": "资源cpu限制", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源cpu限制(示例:100m)" + }, + { + "name": "source_selflabel", + "title": "资源自身标签", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "资源自身标签(service代理)" + }, + { + "name":"source_portmode", + "label":"端口映射模式", + "data":[ + { + "value":"NodePort", + "text":"NodePort" + }, + { + "value":"LoadBalancer", + "text":"LoadBalancer" + }, + { + "value":"Ingress", + "text":"Ingress" + } + ], + "value":"NodePort", + "uitype":"code" + }, + { + "name":"source_restartpolicy", + "label":"重启策略", + "data":[ + { + "value":"Always", + "text":"Always" + }, + { + "value":"OnFailure", + "text":"OnFailure" + }, + { + "value":"Never", + "text":"Never" + } + ], + "value":"Always", + "uitype":"code" + }, + { + "name": "source_apiport", + "title": "集群内部映射端口", + "type": "short", + "length": 0, + "uitype": "int", + "cwidth": 18, + "datatype": "str", + "label": "集群内部映射端口" + }, + { + "name": "source_insideport", + "title": "容器默认监听端口", + "type": "short", + "length": 0, + "uitype": "int", + "cwidth": 18, + "datatype": "str", + "label": "容器默认监听端口" + }, + { + "name": "source_outsideport", + "title": "集群外部映射端口", + "type": "short", + "length": 0, + "uitype": "int", + "cwidth": 18, + "datatype": "str", + "label": "集群外部映射端口(必须是30000-32767之间的整数)" + }, + { + "name":"source_nodeSelector", + "label":"节点标签(指定节点运行)", + "uitype":"code", + "textField":"ip", + "valueField":"id", + "dataurl":"{{entire_url('get_node_labels.dspy')}}?clusterid={{params_kw.id}}" + }, + { + "name":"source_mountpath", + "title":"容器内挂载点", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label":"容器内挂载点" + }, + { + "name": "source_storagelimits", + "title": "定义存储盘容量(单位:Gi)", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "定义存储盘容量(单位:Gi)" + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"submit", + "actiontype":"urlwidget", + "target":"PopupWindow", + "options":{ + "url":"{{entire_url('new_cpcpodyaml.dspy')}}" + } + } + ] +} diff --git a/wwwroot/cpcpodyaml/delete_cpcpodyaml.dspy b/wwwroot/cpcpodyaml/delete_cpcpodyaml.dspy new file mode 100644 index 0000000..80fc916 --- /dev/null +++ b/wwwroot/cpcpodyaml/delete_cpcpodyaml.dspy @@ -0,0 +1,138 @@ + +ns = { + 'id':params_kw['id'], +} + +yamlconfig_id = params_kw.get("id") + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + + # 成功完成k8s资源级联删除后才可以删除数据库记录 + yaml_configs = await sor.R('yaml_config', {'id':yamlconfig_id}) + if len(yaml_configs) < 1: + e = Exception(f'yaml_config {yamlconfig_id=} not exists') + exception(f'{e}') + raise e + yaml_config = yaml_configs[0] + + source_podengine = yaml_config.get("source_podengine") + clusterid = yaml_config.get("clusterid") + cpcid = yaml_config.get("cpcid") + debug(f'===== delete yamlconfig_id: {yamlconfig_id}') + + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + # 此处的nodeid即控制节点id + kubeconfig = cpcluster.get("kubeconfig") + nodeid = cpcluster.get("controllerid") + + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/delete_cpcpod" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + userorgid = await get_userorgid() + debug(f'当前组织ID:{userorgid}') + if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } + + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + ns_name = clusterid.replace("_","-") + "-" + userorgid # 集群信息+组织信息合成命名空间 + ns_name = ns_name.lower() + hc = HttpClient() + + # type参数:更新和新增都是为apply,删除为delete + yamlconfig_id = params_kw.get('id') + + import requests + params = { + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'kubeconfig':kubeconfig, + 'action':'delete', + 'namespace_name': ns_name, + 'serviceaccount_name': ns_name + "-serviceaccount", + 'podcd_name': yamlconfig_id + "-" + source_podengine.lower(), + 'service_name': yamlconfig_id + "-service", #取代+ "-" + source_name + } + params.update(yaml_config) + + keys_to_remove = ['_webbricks_', 'width', 'height', '_is_mobile'] + for key in keys_to_remove: + if key in params: + del params[key] + + debug(f'请求pcapi参数: {params}') + + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data = params, + timeout = 500 + ) + + debug(f'{type(resp)}->{resp}') + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + data = json.loads(resp).get('data') + keys_to_remove = ['host', 'port', 'user', 'psssword', 'kubeconfig'] + for key in keys_to_remove: + if key in params: + del params[key] + params['id'] = yamlconfig_id + + if json.loads(resp).get("status") == True: + + r = await sor.D('yaml_config', ns) + debug('级联删除集群资源成功'); + return { + "widgettype":"Message", + "options":{ + "title":"级联删除集群资源成功", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok" + } + } + +debug('级联删除集群资源失败'); +return { + "widgettype":"Error", + "options":{ + "title":"级联删除集群资源失败", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcpodyaml/get_cpcpodyaml.dspy b/wwwroot/cpcpodyaml/get_cpcpodyaml.dspy new file mode 100644 index 0000000..685ebce --- /dev/null +++ b/wwwroot/cpcpodyaml/get_cpcpodyaml.dspy @@ -0,0 +1,121 @@ + +ns = params_kw.copy() + + +debug(f'get_cpcnode.dspy:{ns=}') +if not ns.get('page'): + ns['page'] = 1 +if not ns.get('sort'): + + ns['sort'] = 'id' + + +sql = '''select * from yaml_config''' + + +filterjson = params_kw.get('data_filter') +if not filterjson: + fields = [ f['name'] for f in [ + { + "name": "id", + "title": "id", + "type": "str", + "length": 32 + }, + { + "name": "name", + "title": "名称", + "type": "str", + "length": 255 + }, + { + "name": "devicetype", + "title": "设备类型", + "type": "str", + "length": 1 + }, + { + "name": "ip", + "title": "内网ip", + "type": "str", + "length": 90 + }, + { + "name": "sshport", + "title": "ssh端口号", + "type": "short" + }, + { + "name": "adminuser", + "title": "管理账号", + "type": "str", + "length": 99 + }, + { + "name": "adminpwd", + "title": "管理密码", + "type": "str", + "length": 99 + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no" + }, + { + "name": "node_status", + "title": "节点状态", + "type": "str", + "length": 1, + "nullable": "yes" + }, + { + "name": "clusterid", + "title": "所属集群", + "type": "long", + "length": 32, + "nullable": "yes" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 255 + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 255 + } +] ] + filterjson = default_filterjson(fields, ns) +filterdic = ns.copy() +filterdic['filterstr'] = '' +filterdic['userorgid'] = '${userorgid}$' +filterdic['userid'] = '${userid}$' +if filterjson: + dbf = DBFilter(filterjson) + conds = dbf.gen(ns) + if conds: + ns.update(dbf.consts) + conds = f' and {conds}' + filterdic['filterstr'] = conds +ac = ArgsConvert('[[', ']]') +vars = ac.findAllVariables(sql) +NameSpace = {v:'${' + v + '}$' for v in vars if v != 'filterstr' } +filterdic.update(NameSpace) +sql = ac.convert(sql, filterdic) + +debug(f'{sql=}') +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.sqlPaging(sql, ns) + return r +return { + "total":0, + "rows":[] +} \ No newline at end of file diff --git a/wwwroot/cpcpodyaml/index.ui b/wwwroot/cpcpodyaml/index.ui new file mode 100644 index 0000000..b624e57 --- /dev/null +++ b/wwwroot/cpcpodyaml/index.ui @@ -0,0 +1,278 @@ +{ + "id": "cpcnode_tbl", + "widgettype": "Tabular", + "options": { + "title": "集群资源实例YAML配置", + "description":"显示非系统命名空间下所有资源实例对应的静态YAML参数,更新资源YAML后,kubernetes集群会根据整体资源情况,自动调整资源实例", + "css": "card", + "editable": { + "new_data_url": "{{entire_url('add_cpcpodyaml.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpcpodyaml.dspy')}}", + "update_data_url": "{{entire_url('update_cpcpodyaml.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpcpodyaml.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "cpcid", + "clusterid" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "cpcid", + "clusterid" + ], + "fields": [ + { + "name": "id", + "title": "资源级联标识", + "type": "str", + "length": 32, + "cwidth": 12, + "uitype": "str", + "datatype": "str", + "label": "资源级联标识" + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "算力中心id" + }, + { + "name": "clusterid", + "title": "所属集群", + "type": "long", + "length": 0, + "nullable": "yes", + "cwidth": 18, + "uitype": "int", + "datatype": "long", + "label": "所属集群" + }, + { + "name": "source_name", + "title": "资源名称", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "资源名称" + }, + { + "name": "namespace_name", + "title": "命名空间", + "type": "str", + "length": 255, + "cwidth": 14, + "uitype": "str", + "datatype": "str", + "label": "命名空间" + }, + { + "name": "serviceaccount_name", + "title": "服务认证", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "服务认证" + }, + { + "name": "podcd_name", + "title": "资源控制器名称", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "资源控制器名称" + }, + { + "name": "instance_type", + "title": "资源实例类型", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "资源实例类型" + }, + { + "name": "service_name", + "title": "服务名称", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "服务名称" + }, + { + "name": "source_replicasetnum", + "title": "副本数量", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "副本数量" + }, + { + "name": "source_authuser", + "title": "默认账号", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "默认账号" + }, + { + "name": "source_authpasswd", + "title": "默认密码", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "默认密码" + }, + { + "name": "source_podengine", + "title": "资源控制器", + "type": "str", + "length": 255, + "cwidth": 7, + "uitype": "str", + "datatype": "str", + "label": "资源控制器" + }, + { + "name": "pod_imagepath", + "title": "基础镜像", + "type": "str", + "length": 255, + "cwidth": 16, + "uitype": "str", + "datatype": "str", + "label": "基础镜像" + }, + { + "name": "source_memrate", + "title": "内存限制", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "内存限制" + }, + { + "name": "source_cpurate", + "title": "CPU限制", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "CPU限制" + }, + { + "name": "source_selflabel", + "title": "资源标签", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "资源标签" + }, + { + "name": "source_portmode", + "title": "端口映射模式", + "type": "str", + "length": 255, + "cwidth": 7, + "uitype": "str", + "datatype": "str", + "label": "端口映射模式" + }, + { + "name": "source_restartpolicy", + "title": "重启策略", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "重启策略" + }, + { + "name": "source_apiport", + "title": "集群内部映射端口", + "type": "str", + "length": 255, + "cwidth": 9, + "uitype": "str", + "datatype": "str", + "label": "集群内部映射端口" + }, + { + "name": "source_insideport", + "title": "容器默认监听端口", + "type": "str", + "length": 255, + "cwidth": 9, + "uitype": "str", + "datatype": "str", + "label": "容器默认监听端口" + }, + { + "name": "source_outsideport", + "title": "集群外部映射端口", + "type": "str", + "length": 255, + "cwidth": 9, + "uitype": "str", + "datatype": "str", + "label": "集群外部映射端口" + }, + { + "name": "source_mountpath", + "title": "容器内部挂载点", + "type": "str", + "length": 255, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "容器内部挂载点" + }, + { + "name": "source_storagelimits", + "title": "容器存储盘限制", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "容器存储盘限制" + } + ] + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [] +} \ No newline at end of file diff --git a/wwwroot/cpcpodyaml/update_cpcpodyaml.dspy b/wwwroot/cpcpodyaml/update_cpcpodyaml.dspy new file mode 100644 index 0000000..bf53604 --- /dev/null +++ b/wwwroot/cpcpodyaml/update_cpcpodyaml.dspy @@ -0,0 +1,110 @@ + +ns = params_kw.copy() +# cpcid +# clusterid + +if params_kw.get('adminpwd'): + ns['adminpwd'] = password_encode(params_kw.get('adminpwd')) + +clusterid = params_kw.get("clusterid") +cpcid = params_kw.get("cpcid") + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + + # 补充['cluster_type', 'host', 'port', 'user', 'psssword', 'kubeconfig'] + + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + # 此处的nodeid即控制节点id + kubeconfig = cpcluster.get("kubeconfig") + nodeid = cpcluster.get("controllerid") + + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/update_cpcpod" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + userorgid = await get_userorgid() + debug(f'当前组织ID:{userorgid}') + if not userorgid: + return { + "widgettype":"Error", + "options":{ + "title":"Authorization Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"Please login" + } + } + + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + ns_name = clusterid.replace("_","-") + "-" + userorgid # 集群信息+组织信息合成命名空间 + ns_name = ns_name.lower() + hc = HttpClient() + # type参数:更新和新增都是为apply,删除为delete + yamlconfig_id = params_kw.get('id') + import requests + params = { + 'cluster_type': cluster_type, + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'kubeconfig':kubeconfig, + 'action':'apply', + 'namespace_name': ns_name, + 'serviceaccount_name': ns_name + "-serviceaccount", + 'podcd_name': yamlconfig_id + "-" + params_kw.get("source_podengine").lower(), + 'service_name': yamlconfig_id + "-service", #取代+ "-" + params_kw.get("source_name") + } + params.update(params_kw) + + keys_to_remove = ['_webbricks_', 'width', 'height', '_is_mobile'] + for key in keys_to_remove: + if key in params: + del params[key] + + debug(f'请求pcapi参数: {params=}') + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + debug(f'{type(resp)=}->{resp=}') + data = json.loads(resp).get('data') + keys_to_remove = ['cluster_type', 'host', 'port', 'user', 'psssword', 'kubeconfig'] + for key in keys_to_remove: + if key in params: + del params[key] + params['id'] = yamlconfig_id + if json.loads(resp).get("status") == True: + + r = await sor.U('yaml_config', ns) + debug('update success'); + return UiMessage(title='更新YAML参数', message='资源参数更新成功,请10秒后查看实时资源实例面板') + else: + return UiError(title='更新YAML参数', message='资源实例更新失败') + +return UiError(title='更新YAML参数', message='其他错误') \ No newline at end of file diff --git a/wwwroot/cpcworker/add_cpcworker.dspy b/wwwroot/cpcworker/add_cpcworker.dspy new file mode 100644 index 0000000..361a2ba --- /dev/null +++ b/wwwroot/cpcworker/add_cpcworker.dspy @@ -0,0 +1,35 @@ + +ns = params_kw.copy() +id = params_kw.id +if not id or len(id) > 32: + id = uuid() +ns['id'] = id + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.C('cpccluster', ns.copy()) + return { + "widgettype":"Message", + "options":{ + "user_data":ns, + "cwidth":16, + "cheight":9, + "title":"Add Success", + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Add Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcworker/delete_cpcworker.dspy b/wwwroot/cpcworker/delete_cpcworker.dspy new file mode 100644 index 0000000..3d40fe8 --- /dev/null +++ b/wwwroot/cpcworker/delete_cpcworker.dspy @@ -0,0 +1,33 @@ + +ns = { + 'id':params_kw['id'], +} + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.D('cpccluster', ns) + debug('delete success'); + return { + "widgettype":"Message", + "options":{ + "title":"Delete Success", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"ok,记得把关联的节点都解除占用状态 ~" + } + } + +debug('Delete failed'); +return { + "widgettype":"Error", + "options":{ + "title":"Delete Error", + "timeout":3, + "cwidth":16, + "cheight":9, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/cpcworker/get_availableworker.dspy b/wwwroot/cpcworker/get_availableworker.dspy new file mode 100644 index 0000000..baca0d7 --- /dev/null +++ b/wwwroot/cpcworker/get_availableworker.dspy @@ -0,0 +1,11 @@ +db = DBPools() +dbname = get_module_dbname('cpcc') +ns = { + "clusterid":params_kw.clusterid +} +async with db.sqlorContext(dbname) as sor: + sql = "select cpcid,id,ip,node_status from cpcnode where cpcid = (select cpcid from cpccluster where id = ${clusterid}$) and node_status = '0';" + recs = await sor.sqlExe(sql, ns.copy()) + return recs +exception(f'{sql=},{ns=}') +return [] \ No newline at end of file diff --git a/wwwroot/cpcworker/get_cpcworker.dspy b/wwwroot/cpcworker/get_cpcworker.dspy new file mode 100644 index 0000000..698c08f --- /dev/null +++ b/wwwroot/cpcworker/get_cpcworker.dspy @@ -0,0 +1,84 @@ +ns = params_kw.copy() + +debug(f'get_cpcworker.dspy:{ns=}') +#{'_webbricks_': '1', 'width': '1139', 'height': '940', '_is_mobile': '0', +#'clusterid': '4hBm8atruISOU2bs24t_N', 'cpcid': 'AROU9udKtPNyh0AZtO_WY', 'page': '1', 'rows': '160'} + +cpcid = params_kw.cpcid +clusterid = params_kw.clusterid + +# 写数据库的步骤先pass +dbname = get_module_dbname('cpcc') +db = DBPools() + +async with db.sqlorContext(dbname) as sor: + # 通过算力中心ID,获取算力中心部署所在设备的http(s)协议信息 + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + # 通过集群ID,获取算力集群控制节点所在设备的ssh协议参数 + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + nodeid = cpcluster.get("controllerid") + kubeconfig = cpcluster.get("kubeconfig") + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/get_cluster_nodes" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + + hc = HttpClient() + + import requests + params = { + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'kubeconfig':kubeconfig + } + debug(f'请求参数{params=}') + + #resp = await hc.request(url, method='GET', + # headers = headers, + # data=params + #) + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + #debug(f'{type(resp)=}->{resp=}') + debug(f"pcapi返回值: {json.loads(resp)=}") + data = json.loads(resp).get('data') + if json.loads(resp).get("status") == True: + #node_ns = { + # 'id':nodeid, + # 'node_status':'1', + # 'clusterid':clusterid, + # 'cpcid':cpcid + #} + #debug(f"更新新增工作节点元数据 {node_ns=}") + #await sor.U('cpcnode', node_ns) + return data + else: + return UiError(title='get cluster nodes', message='failed') + +return UiError(title='get cluster nodes', message='uncatched error') \ No newline at end of file diff --git a/wwwroot/cpcworker/index.ui b/wwwroot/cpcworker/index.ui new file mode 100644 index 0000000..e9f47c5 --- /dev/null +++ b/wwwroot/cpcworker/index.ui @@ -0,0 +1,179 @@ +{ + "id": "cpcworker_tbl", + "widgettype": "Tabular", + "options": { + "title": "集群实时节点", + "description":"显示集群中所有实时节点信息", + "css": "card", + "editable": { + "new_data_url": "{{entire_url('add_cpcworker.dspy')}}", + "delete_data_url": "{{entire_url('delete_cpcworker.dspy')}}", + "update_data_url": "{{entire_url('update_cpcworker.dspy')}}" + }, + "data_url": "{{entire_url('./get_cpcworker.dspy')}}", + "data_method": "GET", + "data_params": {{json.dumps(params_kw, indent=4, ensure_ascii=False)}}, + "row_options": { + "browserfields": { + "exclouded": [ + "id", + "cpcid", + "clusterid" + ], + "alters": {} + }, + "editexclouded": [ + "id", + "cpcid", + "clusterid" + ], + "fields": [ + { + "name": "node_internalip", + "title": "内网IP", + "type": "str", + "length": 255, + "cwidth": 6, + "uitype": "str", + "datatype": "str", + "label": "内网IP" + }, + { + "name": "node_name", + "title": "主机名", + "type": "str", + "length": 255, + "cwidth": 15, + "uitype": "str", + "datatype": "str", + "label": "主机名" + }, + { + "name": "node_status", + "title": "运行状态", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "运行状态" + }, + { + "name": "id", + "title": "id", + "type": "str", + "length": 32, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "id" + }, + { + "name": "cpcid", + "title": "算力中心id", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "算力中心id" + }, + { + "name": "node_role", + "title": "节点角色", + "type": "str", + "length": 32, + "nullable": "no", + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "节点角色" + }, + { + "name": "node_age", + "title": "运行时长", + "type": "str", + "length": 255, + "cwidth": 5, + "uitype": "str", + "datatype": "str", + "label": "运行时长" + }, + { + "name": "node_version", + "title": "kubernetes版本", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "kubernetes版本" + }, + { + "name": "node_cpurate", + "title": "CPU占用率", + "type": "str", + "length": 255, + "cwidth": 6, + "uitype": "str", + "datatype": "str", + "label": "CPU占用率" + }, + { + "name": "node_memrate", + "title": "内存占用率", + "type": "str", + "length": 255, + "cwidth": 6, + "uitype": "str", + "datatype": "str", + "label": "内存占用率" + }, + { + "name": "node_osversion", + "title": "操作系统", + "type": "str", + "length": 255, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "操作系统" + }, + { + "name": "node_containeruntime", + "title": "容器运行时版本", + "type": "str", + "length": 255, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "容器运行时" + }, + { + "name": "node_kernelversion", + "title": "内核版本", + "type": "str", + "length": 255, + "cwidth": 10, + "uitype": "str", + "datatype": "str", + "label": "内核版本" + }, + { + "name": "node_externalip", + "title": "外网IP", + "type": "str", + "length": 255, + "cwidth": 8, + "uitype": "str", + "datatype": "str", + "label": "外网IP" + } + ] + }, + "page_rows": 160, + "cache_limit": 5 + }, + "binds": [] +} \ No newline at end of file diff --git a/wwwroot/cpcworker/new_cpcworker.dspy b/wwwroot/cpcworker/new_cpcworker.dspy new file mode 100644 index 0000000..35f3aaa --- /dev/null +++ b/wwwroot/cpcworker/new_cpcworker.dspy @@ -0,0 +1,83 @@ +# params_kw: +# clusterid +# worker_nodeid +debug(f'{params_kw=}') +dbname = get_module_dbname('cpcc') +db = DBPools() +clusterid = params_kw.clusterid +nodeid = params_kw.worker_nodeid +#debug(f"=====> {clusterid=} {worker_nodeid=}") + +async with db.sqlorContext(dbname) as sor: + # clusterid -> cpcid + cpclusters = await sor.R('cpccluster', {'id':clusterid}) + if len(cpclusters) < 1: + e = Exception(f'cpclist {clusterid=} not exists') + exception(f'{e}') + raise e + cpcluster = cpclusters[0] + join_command = cpcluster.get("clusterjoin") + cpcid = cpcluster.get("cpcid") + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + debug(f'集群注册命令:{join_command=}') + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/new_worker" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + + headers = basic_auth_headers(cpc.api_user, password_decode(cpc.api_pwd)) + + hc = HttpClient() + + import requests + params = { + 'cluster_type': cluster_type, + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'role':"worker", + 'join_command':join_command + } + debug(f'{params=}') + + #resp = await hc.request(url, method='POST', + # headers = headers, + # data=params + #) + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + debug(f'{type(resp)=}->{resp=}') + debug(f"pcapi返回值: {json.loads(resp)=}") + data = json.loads(resp).get('data') + if json.loads(resp).get("status") == True: + node_ns = { + 'id':nodeid, + 'node_status':'1', + 'clusterid':clusterid, + 'cpcid':cpcid + } + debug(f"更新新增工作节点元数据 {node_ns=}") + await sor.U('cpcnode', node_ns) + return UiMessage(title='new cluster', message='操作成功!') + else: + return UiError(title='new cluster', message='failed') + +return UiError(title='new cluster', message='uncatched error') diff --git a/wwwroot/cpcworker/new_worker.ui b/wwwroot/cpcworker/new_worker.ui new file mode 100644 index 0000000..20f978c --- /dev/null +++ b/wwwroot/cpcworker/new_worker.ui @@ -0,0 +1,33 @@ +{ + "widgettype":"Form", + "options":{ + "title":"新增工作节点", + "description":"为算力中心的kubernetes集群新增工作节点,目前支持kubernetes工作模式", + "fields":[ + { + "name":"clusterid", + "value":"{{params_kw.id}}", + "uitype":"hide" + }, + { + "name":"worker_nodeid", + "label":"请选择安装工作模式的节点", + "uitype":"code", + "textField":"ip", + "valueField":"id", + "dataurl":"{{entire_url('get_availableworker.dspy')}}?clusterid={{params_kw.id}}" + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"submit", + "actiontype":"urlwidget", + "target":"PopupWindow", + "options":{ + "url":"{{entire_url('new_cpcworker.dspy')}}" + } + } + ] +} diff --git a/wwwroot/cpcworker/update_cpcworker.dspy b/wwwroot/cpcworker/update_cpcworker.dspy new file mode 100644 index 0000000..af390dd --- /dev/null +++ b/wwwroot/cpcworker/update_cpcworker.dspy @@ -0,0 +1,32 @@ + +ns = params_kw.copy() + + + + +db = DBPools() +dbname = await rfexe('get_module_dbname', 'cpcc') +async with db.sqlorContext(dbname) as sor: + r = await sor.U('cpccluster', ns) + debug('update success'); + return { + "widgettype":"Message", + "options":{ + "title":"Update Success", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"ok" + } + } + +return { + "widgettype":"Error", + "options":{ + "title":"Update Error", + "cwidth":16, + "cheight":9, + "timeout":3, + "message":"failed" + } +} \ No newline at end of file diff --git a/wwwroot/handy/get_cpcnodes.dspy b/wwwroot/handy/get_cpcnodes.dspy new file mode 100644 index 0000000..67a740e --- /dev/null +++ b/wwwroot/handy/get_cpcnodes.dspy @@ -0,0 +1,12 @@ +debug(f'{params_kw=}') +db = DBPools() +dbname = get_module_dbname('cpcc') +ns = { + "cpcid":params_kw.cpcid +} +async with db.sqlorContext(dbname) as sor: + sql = "select * from cpcnode where cpcid=${cpcid}$ and node_status = '0'" + recs = await sor.sqlExe(sql, ns.copy()) + return recs +exception(f'{sql=},{ns=}') +return [] diff --git a/wwwroot/handy/new_cluster.dspy b/wwwroot/handy/new_cluster.dspy new file mode 100644 index 0000000..2307dfe --- /dev/null +++ b/wwwroot/handy/new_cluster.dspy @@ -0,0 +1,97 @@ +# params_kw: +# cpcid +# cluster_type +# ctl_nodeid +# cluster_name +debug(f'{params_kw=}') +dbname = get_module_dbname('cpcc') +db = DBPools() +cpcid = params_kw.cpcid +nodeid = params_kw.ctl_nodeid +cluster_type = params_kw.cluster_type +cluster_name = params_kw.cluster_name +enable_date = params_kw.enable_date +export_date = params_kw.export_date +# debug(f"=====> {cluster_type=} {cluster_name=} {nodeid=} {cpcid=}") + +async with db.sqlorContext(dbname) as sor: + cpcs = await sor.R('cpclist', {'id':cpcid}) + if len(cpcs) < 1: + e = Exception(f'cpclist {cpcid=} not exists') + exception(f'{e}') + raise e + cpc = cpcs[0] + nodes = await sor.R('cpcnode', {'id':nodeid}) + # 这里有问题:没有匹配到节点信息!!! + if len(nodes) < 1: + e = Exception(f'cpcnode {nodeid=} 节点基础信息不匹配') + exception(f'{e}') + raise e + node = nodes[0] + url = cpc.pcapi_url + "/pcapi/api/v1/cluster/common/new_cluster" + debug(f"请求url: {url=}") + debug(f"目标IP认证信息: {node.ip=} {node.sshport=} {node.adminuser=} {password_decode(node.adminpwd)=}") + # 请求方式待定,取决于获取参数值方式 + debug(333) + headers = basic_auth_headers('ysh', 'Kyy@123456') + debug(22) + hc = HttpClient() + print(444) + import requests + params = { + 'cluster_type': cluster_type, + 'host':node.ip, + 'port':node.sshport, + 'user':node.adminuser, + 'psssword':password_decode(node.adminpwd), + 'role':"master" + } + debug(f'{params=}') + debug(1111111) + #resp = await hc.request(url, method='POST', + # headers = headers, + # data=params + #) + # 框架不支持超时时间 + resp = requests.post(url, + headers = headers, + data=params, + timeout=500 + ) + resp = json.dumps(resp.json()) #这里模拟hc.request返回的结果写后续逻辑 + debug(f'{type(resp)=}->{resp=}') + debug(f"pcapi返回值: {json.loads(resp)=}") + 2datas = json.loads(resp).get('data') + 2datas = 2datas.split("\n") + clusterjoin = 2datas[0] + kubeconfig_context = 2datas[1] + if json.loads(resp).get("status") == True: + # new cluster info write to database + # update node_status to '1' cpcnode record identify by ctl_nodeid ctl_node + clusterid = uuid() + ns = { + 'id':clusterid, + 'clustertype':cluster_type, + 'cpcid':cpcid, + 'controllerid':nodeid, + 'name':cluster_name, + 'enable_date':enable_date, + 'export_date':export_date, + 'clusterjoin':clusterjoin, + 'kubeconfig':kubeconfig_context + } + debug(f"新集群元数据: {ns=}") + await sor.C('cpccluster', ns) + node_ns = { + 'id':nodeid, + 'node_status':'1', + 'clusterid':clusterid, + 'cpcid':cpcid + } + debug(f"更新控制节点元数据 {node_ns=}") + await sor.U('cpcnode', node_ns) + return UiMessage(title='new cluster', message='操作成功!\n加入集群凭证: %s' % clusterjoin) + else: + return UiError(title='new cluster', message='failed') + +return UiError(title='new cluster', message='uncatched error') diff --git a/wwwroot/handy/new_cluster.ui b/wwwroot/handy/new_cluster.ui new file mode 100644 index 0000000..1bbaf78 --- /dev/null +++ b/wwwroot/handy/new_cluster.ui @@ -0,0 +1,79 @@ +{ + "widgettype":"Form", + "options":{ + "title":"创建集群", + "description":"为算力中心创建集群,目前支持kubernetes集群", + "fields":[ + { + "name":"cpcid", + "value":"{{params_kw.id}}", + "uitype":"hide" + }, + { + "name":"cluster_type", + "label":"集群类型", + "data":[ + { + "value":"0", + "text":"kubernetes" + }, + { + "value":"1", + "text":"slurm" + } + ], + "value":"0", + "uitype":"code" + }, + { + "name": "cluster_name", + "title": "集群名称", + "type": "str", + "length": 90, + "cwidth": 18, + "uitype": "str", + "datatype": "str", + "label": "集群名称" + }, + { + "name":"ctl_nodeid", + "label":"控制节点IP", + "uitype":"code", + "textField":"ip", + "valueField":"id", + "dataurl":"{{entire_url('get_cpcnodes.dspy')}}?cpcid={{params_kw.id}}" + }, + { + "name": "enable_date", + "title": "启用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "启用日期" + }, + { + "name": "export_date", + "title": "停用日期", + "type": "date", + "length": 0, + "cwidth": 18, + "uitype": "date", + "datatype": "date", + "label": "停用日期" + } + ] + }, + "binds":[ + { + "wid":"self", + "event":"submit", + "actiontype":"urlwidget", + "target":"PopupWindow", + "options":{ + "url":"{{entire_url('new_cluster.dspy')}}" + } + } + ] +} diff --git a/wwwroot/index.ui b/wwwroot/index.ui new file mode 100644 index 0000000..2cbe938 --- /dev/null +++ b/wwwroot/index.ui @@ -0,0 +1,27 @@ +{ + "widgettype":"VBox", + "options":{ + "width":"100%", + "height":"100%" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('top.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('center.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('bottom.ui')}}" + } + } + ] +} diff --git a/wwwroot/menu.ui b/wwwroot/menu.ui new file mode 100644 index 0000000..0ddca6d --- /dev/null +++ b/wwwroot/menu.ui @@ -0,0 +1,20 @@ +{% set roles = get_user_roles(get_user()) %} +{ + "widgettype":"Menu", + "options":{ + "target":"page_center", + "cwidth":10, + "items":[ + { + "name":"home", + "label":"代码管理", + "url":"{{entire_url('/appbase/appcodes')}}" + }, + { + "name":"home", + "label":"算力中心管理", + "url":"{{entire_url('cpclist')}}" + } + ] + } +} diff --git a/wwwroot/top.ui b/wwwroot/top.ui new file mode 100644 index 0000000..2684873 --- /dev/null +++ b/wwwroot/top.ui @@ -0,0 +1,23 @@ +{ + "id":"top_panel", + "widgettype":"HBox", + "options":{ + "bgcolor":"#444444", + "cheight":2.5, + "color":"#eeeeee" + }, + "subwidgets":[ + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('app_panel.ui')}}" + } + }, + { + "widgettype":"urlwidget", + "options":{ + "url":"{{entire_url('/rbac/user/user_panel.ui')}}" + } + } + ] +}