Compare commits
466 Commits
codex/setu
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 55a05da3bf | |||
| 477b52c516 | |||
| 4364786465 | |||
| e953d87f07 | |||
|
|
d806ba9d3d | ||
| a2ce5b9d05 | |||
| 19a3c9f72a | |||
|
|
5c74feb860 | ||
|
|
9b59c89d80 | ||
|
|
abee312617 | ||
|
|
cd9a783de7 | ||
|
|
8fcff61855 | ||
|
|
5d00d700ca | ||
|
|
50dba213ee | ||
|
|
c62386f30c | ||
|
|
29e60383e3 | ||
|
|
e174e8bcfa | ||
|
|
5aadb4f0dc | ||
|
|
c9919284e0 | ||
|
|
5984a75643 | ||
|
|
c7bc68a6dc | ||
|
|
609a88ddcf | ||
|
|
40b7975061 | ||
|
|
3709074916 | ||
|
|
c3a0e40566 | ||
|
|
c3f3b8ac8e | ||
|
|
2ef144d572 | ||
|
|
3505ff1c31 | ||
|
|
a5e19eff60 | ||
|
|
df48cb4f5a | ||
|
|
099a144a9e | ||
|
|
f5a5979439 | ||
|
|
e5fc29fa8a | ||
|
|
9e81f65a62 | ||
| e0bfc765bf | |||
| 4e183d2d44 | |||
| 28df3b59d6 | |||
| a0d59c0af1 | |||
| 25b8204b7b | |||
| 6e260a3425 | |||
| e7c96675ff | |||
|
|
01f1499a60 | ||
|
|
a5850cfcee | ||
|
|
2a85be5c9b | ||
|
|
32e00a8617 | ||
|
|
0ac424f00e | ||
|
|
1b2aea005a | ||
|
|
93a3067ea4 | ||
|
|
9926a46f76 | ||
|
|
ef67c61cf7 | ||
|
|
6091b9dbcf | ||
|
|
d9033960fd | ||
|
|
bbf5260f0d | ||
|
|
ce2070e779 | ||
| f4a30b9e01 | |||
|
|
6a2f05f435 | ||
|
|
71ebe6444c | ||
|
|
c11f51b4c9 | ||
|
|
f01e0bb15b | ||
|
|
09a39e69ee | ||
|
|
02667f9e76 | ||
|
|
65e45a4834 | ||
|
|
f231867593 | ||
|
|
1dd0ce2e04 | ||
|
|
9f04d4d9b5 | ||
|
|
4f87b67a4e | ||
|
|
51d08cf9db | ||
|
|
aa3b4e8069 | ||
|
|
85bad4155f | ||
|
|
48ba854671 | ||
|
|
fa04606542 | ||
|
|
aedf457ddc | ||
|
|
5f7bc697fc | ||
|
|
284c3c43a3 | ||
|
|
340de0c4d8 | ||
|
|
ae9f09d77f | ||
|
|
a170671ffd | ||
|
|
a35befd123 | ||
|
|
2a8db4f79a | ||
|
|
aa59be12a0 | ||
|
|
4863c327cc | ||
|
|
16ecda9e7d | ||
|
|
32da386051 | ||
|
|
8d644f006e | ||
|
|
f6ef2202b2 | ||
|
|
73e33e3083 | ||
|
|
3f1ece8601 | ||
|
|
68206296f4 | ||
|
|
a5db649802 | ||
|
|
21e0ab9628 | ||
|
|
04bf000c78 | ||
|
|
ffa357ac4e | ||
|
|
fd142df681 | ||
| 321fd81f37 | |||
| 8e346889fe | |||
| c14962d572 | |||
| e51e07f210 | |||
| 4ea7adbfa2 | |||
|
|
cc2e010377 | ||
|
|
4d63a66cf4 | ||
| c07874b4d4 | |||
| 784f683a3b | |||
| 0cfd1af1b7 | |||
| 17e2267449 | |||
|
|
cdbfb2e92a | ||
|
|
6aa240c16b | ||
|
|
a3e570371a | ||
|
|
f7800111b2 | ||
|
|
7635677dbf | ||
|
|
4164e1ff91 | ||
|
|
f66a118c57 | ||
|
|
a0b27a7aee | ||
|
|
51565ecf66 | ||
|
|
402c90967a | ||
|
|
7613a848a2 | ||
|
|
5facdd5331 | ||
|
|
edc70fb658 | ||
|
|
45f6f3af89 | ||
| e4aa8affee | |||
|
|
d876d69684 | ||
| 40395ba0a2 | |||
|
|
c57642dce2 | ||
| 9ce92f6cf2 | |||
|
|
06e10fb0e1 | ||
| d745e26188 | |||
|
|
7c5884c615 | ||
| c56ac0561c | |||
| 51d28b5d8b | |||
| b85a80b8f8 | |||
| a7ad856e05 | |||
|
|
7dcd2307ea | ||
| de143241c8 | |||
|
|
33ef20e064 | ||
| 7938e485a5 | |||
|
|
7e886ec009 | ||
| 014cc06824 | |||
|
|
740b0a5e72 | ||
| 15ffca6368 | |||
|
|
091cd1bfc1 | ||
| 3d03134a62 | |||
|
|
bd6de2ba7b | ||
| 735132ea25 | |||
|
|
4ca20c8603 | ||
|
|
e83c1a73ac | ||
|
|
39bdc7c1fd | ||
|
|
8a62cc4e59 | ||
|
|
4a60ff30e4 | ||
|
|
a3fd2679ef | ||
|
|
74d027e649 | ||
|
|
aabf296461 | ||
|
|
044a264256 | ||
|
|
c7784f2063 | ||
|
|
5e3db5dfd5 | ||
|
|
75c4c98613 | ||
|
|
2946a7bc42 | ||
|
|
dbbce5ff49 | ||
|
|
0e1f8ab7cf | ||
|
|
532c57a359 | ||
|
|
c1162f7ea2 | ||
|
|
13d986a078 | ||
|
|
5e363249ce | ||
|
|
1ac560e482 | ||
|
|
b36a1c44e5 | ||
|
|
e5991301c6 | ||
|
|
3809a8cb6b | ||
|
|
596f52ba12 | ||
|
|
d49b472ddb | ||
|
|
93cbe2cd1b | ||
|
|
5630df788a | ||
|
|
7936f65485 | ||
|
|
1c6ebc36ba | ||
|
|
c07d12b5fe | ||
|
|
e4b04f95fe | ||
|
|
d92979f22d | ||
|
|
2658727d19 | ||
|
|
dcf49e4ebf | ||
|
|
ba4ef489aa | ||
|
|
126a19e282 | ||
|
|
c627f016bf | ||
|
|
5f00409550 | ||
|
|
40ed86a070 | ||
|
|
178664f262 | ||
|
|
2243b5d0c8 | ||
|
|
65aef78937 | ||
|
|
2f4d3ad930 | ||
|
|
39dbb7b5f9 | ||
|
|
3793143466 | ||
|
|
437d50c095 | ||
|
|
981d83acab | ||
|
|
4228c1a6df | ||
|
|
cfe89432a1 | ||
|
|
645ac9bd17 | ||
|
|
3084ab7940 | ||
|
|
f15c384a34 | ||
|
|
6346684af5 | ||
|
|
bfb6b17e29 | ||
|
|
2319c592fb | ||
|
|
41853eedd9 | ||
|
|
5e359cc5d8 | ||
|
|
4b6b1de8a7 | ||
|
|
0b344b5bd0 | ||
|
|
ae78231fac | ||
|
|
cd2d4b0046 | ||
|
|
7f6854e9de | ||
|
|
4c330b7e1c | ||
|
|
a15016ef1f | ||
|
|
e2ae564745 | ||
|
|
4b7c52057d | ||
|
|
f3ab617db6 | ||
|
|
cc41ff61db | ||
|
|
604132e604 | ||
|
|
c784b621f6 | ||
|
|
1f7d85b35d | ||
|
|
60269ee222 | ||
|
|
74b3411336 | ||
|
|
811b17962b | ||
|
|
56b33a3231 | ||
|
|
f424327cfb | ||
|
|
affd6827b0 | ||
|
|
7d1a86e412 | ||
|
|
944d59f911 | ||
|
|
6d6a3a8593 | ||
|
|
b8d4df9230 | ||
|
|
1574287a4d | ||
|
|
e9dec70225 | ||
|
|
e3952916af | ||
|
|
47d4931ff7 | ||
|
|
7ef5005ae1 | ||
|
|
9196625bd0 | ||
|
|
a076370b68 | ||
|
|
21cbbca9be | ||
|
|
c22a8c8266 | ||
|
|
cdf06da6d9 | ||
|
|
a77d2fedfb | ||
|
|
b4ebecc32d | ||
|
|
629016185d | ||
|
|
96ad38ff14 | ||
|
|
c1cb19b59b | ||
|
|
1d8516d160 | ||
|
|
72763856d3 | ||
|
|
9cde355688 | ||
|
|
e6a3d95578 | ||
|
|
814a81f088 | ||
|
|
ed8a78e932 | ||
|
|
d5a17a8301 | ||
|
|
01af16cd54 | ||
|
|
a68cf68d14 | ||
|
|
d57ef6458d | ||
|
|
4a14572b5b | ||
|
|
e2b7f0366c | ||
|
|
fc7a23617c | ||
|
|
fc1bff0061 | ||
|
|
db9d564ef3 | ||
|
|
d573a4651b | ||
|
|
ce6d970bda | ||
|
|
a817a0e732 | ||
|
|
e56cb63032 | ||
|
|
e5efac92e4 | ||
|
|
2252d24708 | ||
| a421eb2e4f | |||
|
|
1414fe588f | ||
|
|
b58a74892c | ||
|
|
aee1f2b5d5 | ||
|
|
466cb29a1b | ||
|
|
b6d18f5944 | ||
|
|
42b8443f91 | ||
|
|
8c71b27112 | ||
|
|
7e0dc61924 | ||
|
|
f451b5cd20 | ||
|
|
6c234f9544 | ||
|
|
6d3418284a | ||
|
|
d7199c511b | ||
|
|
61eb40624d | ||
|
|
dcdc9bea7b | ||
|
|
2f2e9d8f9b | ||
|
|
ba4daa3597 | ||
|
|
402faa02e1 | ||
|
|
ce0dd3cee1 | ||
|
|
e3921518ba | ||
|
|
003d48e748 | ||
|
|
69e7691287 | ||
|
|
71e3449622 | ||
|
|
805a3fbda9 | ||
|
|
22662cc538 | ||
|
|
7fbba293a0 | ||
|
|
f51958a4a2 | ||
|
|
aa674a7dac | ||
|
|
9765158371 | ||
|
|
5ff5e2f1eb | ||
|
|
dfad2a0a5c | ||
|
|
cd131e79f4 | ||
|
|
29dd6a38b7 | ||
|
|
ae1e5813a9 | ||
|
|
4b2ab8401b | ||
|
|
72bee745b3 | ||
|
|
0c3e673e78 | ||
|
|
07f72e2c46 | ||
|
|
ad49ba1b22 | ||
|
|
b6b0e3ddad | ||
|
|
3ae95ea54d | ||
|
|
6c1ad92ff4 | ||
|
|
f023bd3961 | ||
|
|
95efae0060 | ||
|
|
1fa9ca2457 | ||
|
|
9f3449b635 | ||
|
|
289468e188 | ||
|
|
a50dc24619 | ||
|
|
e2bbc56e7a | ||
|
|
dd0201e483 | ||
|
|
d3efb08e8d | ||
|
|
54b234b2bc | ||
|
|
a250cf70e5 | ||
|
|
f6167c1e89 | ||
|
|
14c77e6e5e | ||
|
|
3d091118c2 | ||
|
|
9ba79fb05a | ||
|
|
fd9d42b9a5 | ||
|
|
d08987120a | ||
|
|
176aaf8fcf | ||
|
|
b40003b66d | ||
|
|
e1dc41e54f | ||
|
|
c5f17b1c92 | ||
|
|
1af963699a | ||
|
|
59a7e6be4d | ||
|
|
184a200c40 | ||
|
|
fa98d41b64 | ||
|
|
5f1f765660 | ||
|
|
db60aa1ddf | ||
|
|
aa2b2e0f2d | ||
|
|
3bf305e793 | ||
|
|
966cc16b7f | ||
|
|
ce56e0374b | ||
|
|
5318fc28bd | ||
|
|
5e6477e64c | ||
|
|
e0769d32bc | ||
|
|
7422c9d41f | ||
|
|
bd3624b77b | ||
|
|
92322833d2 | ||
|
|
ef2f77837f | ||
|
|
4dde19987a | ||
|
|
f480dc633b | ||
|
|
515ba95c75 | ||
|
|
413d46995b | ||
|
|
c478863b74 | ||
|
|
827d78543a | ||
|
|
747426eb25 | ||
|
|
73bb2822fd | ||
|
|
cb4a4bc023 | ||
|
|
99ca8b4ee8 | ||
|
|
b1276eee71 | ||
|
|
d375eab837 | ||
|
|
746b9407ff | ||
|
|
3f0e21d237 | ||
|
|
ae5f7c5b4e | ||
|
|
acfe7f564d | ||
|
|
f20980bdc0 | ||
|
|
5fa35235e1 | ||
|
|
ae1d318332 | ||
|
|
cd92dbc20d | ||
|
|
1cbe937178 | ||
|
|
c82c93d9ff | ||
|
|
74384140e2 | ||
|
|
e1a29dc4a0 | ||
|
|
26499f5602 | ||
|
|
c0f1a1c2ee | ||
|
|
97d49eaf39 | ||
|
|
27e19c4457 | ||
|
|
9cc0e6bfb8 | ||
|
|
220203b133 | ||
|
|
04fb63881c | ||
| 80c545a95c | |||
|
|
6c9bd1e0b0 | ||
|
|
130932fc6f | ||
|
|
427eed969e | ||
|
|
4c62883bfc | ||
|
|
335ee6ef81 | ||
|
|
d2531f6a22 | ||
|
|
c90bdd9093 | ||
|
|
68d4554be7 | ||
|
|
bbeaa1c992 | ||
|
|
a1f21c4030 | ||
|
|
677177548e | ||
|
|
6c728d4911 | ||
|
|
e7d9140b86 | ||
|
|
19e1f4ef1d | ||
|
|
b8d93ec31c | ||
|
|
3ce18ef133 | ||
|
|
396a1fad71 | ||
|
|
a209041839 | ||
|
|
9ad2740997 | ||
|
|
c7ffff2825 | ||
|
|
7b4e119030 | ||
|
|
117b912529 | ||
|
|
ac83d810c6 | ||
|
|
e774f5746b | ||
|
|
fb444b23b7 | ||
|
|
32d928a5da | ||
|
|
210e32b6db | ||
|
|
fb0a9dae5e | ||
|
|
8f3f4a07dc | ||
|
|
93e25c07f2 | ||
|
|
6d1f582ea1 | ||
|
|
08330218a6 | ||
|
|
605ead2f2e | ||
|
|
672ea8ba32 | ||
|
|
9d6e59e802 | ||
|
|
557272bf88 | ||
|
|
36813d4bde | ||
|
|
e9ea0b1d3b | ||
|
|
47504726a3 | ||
|
|
d195a21a66 | ||
|
|
0d5371e98b | ||
|
|
b03c1b5797 | ||
|
|
e8515003f3 | ||
|
|
78bc356655 | ||
|
|
2061a3cd4f | ||
|
|
4ae3955d62 | ||
|
|
4a6978c3b5 | ||
|
|
82eadec0c0 | ||
|
|
16abf5a58e | ||
|
|
cd3e9a1afe | ||
|
|
b9f800eedc | ||
|
|
4f6b7069c0 | ||
|
|
0f0b7cfd04 | ||
|
|
f7a627673a | ||
|
|
3a7e30971a | ||
|
|
3f21540ec6 | ||
|
|
16b5c90ee4 | ||
|
|
a8a1abf817 | ||
|
|
bbcbe61abc | ||
|
|
d595ebabfa | ||
|
|
ec9a41bbb0 | ||
|
|
ac32c148ca | ||
|
|
2aae4b62cb | ||
|
|
36b1425365 | ||
|
|
b150174d1b | ||
|
|
9bf9f8a27c | ||
|
|
21b6f54f23 | ||
|
|
f735120d6e | ||
|
|
23012fd3b0 | ||
|
|
8c74e17a5e | ||
|
|
a569fe153b | ||
|
|
18b865d763 | ||
|
|
92785aea6b | ||
|
|
03ce101458 | ||
|
|
ec4a25edb2 | ||
|
|
47249add9f | ||
|
|
2fc3f49fc3 | ||
|
|
10cf7b554c | ||
|
|
ed68e026d3 | ||
|
|
1c06a62d04 | ||
|
|
fa823ca8cd | ||
|
|
f9e8563362 | ||
|
|
076ae599c5 | ||
|
|
9dda0c4d96 | ||
|
|
a68bc0c87c | ||
|
|
686da86fd4 | ||
|
|
ac4e3886aa | ||
| 142a301fc6 | |||
| e4b3c7f7f0 | |||
| 1148aef0a7 | |||
| 10472ea250 | |||
| e6c0d3239c | |||
| 8a033c55d0 | |||
| a906fe11bc | |||
| 89714aeeec |
44
.github/workflows/validate-release-pr.yml
vendored
Normal file
44
.github/workflows/validate-release-pr.yml
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
name: Validate Release PR
|
||||
|
||||
# release/* 分支的发布策略门禁:仅接受 hotfix/* 或带 cherry-pick/backport 标签的 PR。
|
||||
# 详见 iac_modules/docs/tldr-github-branch-model.md
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
validate-release-source:
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.base_ref, 'release/')
|
||||
steps:
|
||||
- name: Check PR source branch
|
||||
run: |
|
||||
SRC="${{ github.head_ref }}"
|
||||
TGT="${{ github.base_ref }}"
|
||||
LABELS="${{ join(github.event.pull_request.labels.*.name, ',') }}"
|
||||
|
||||
echo "🔍 Validating PR into release branch"
|
||||
echo " source: $SRC"
|
||||
echo " target: $TGT"
|
||||
echo " labels: $LABELS"
|
||||
|
||||
if [[ "$SRC" =~ ^hotfix/ ]]; then
|
||||
echo "✅ Allowed: hotfix/* branch"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$LABELS" =~ (^|,)(cherry-pick|backport)(,|$) ]]; then
|
||||
echo "✅ Allowed: cherry-pick/backport labeled PR"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "❌ Rejected."
|
||||
echo "release/* 仅接受:"
|
||||
echo " - 来自 hotfix/* 的 PR"
|
||||
echo " - 带 cherry-pick 或 backport 标签的 PR(已验证 feature 的 backport/cherry-pick)"
|
||||
echo "禁止从 main / develop / feature/* 直接合并到 release/*。"
|
||||
exit 1
|
||||
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
xfce-secrets.yml
|
||||
inventory/__pycache__/
|
||||
.playwright-mcp/
|
||||
.env
|
||||
.artifacts/
|
||||
.artifacts/acp_codex/xworkmate-go-core
|
||||
.artifacts/acp_opencode/xworkmate-go-core
|
||||
3
.gitleaksignore
Normal file
3
.gitleaksignore
Normal file
@ -0,0 +1,3 @@
|
||||
dcdc9bea7b49f045e1ac0a30f85a5e0c84c1e8db:group_vars/xworkmate_bridge_distributed.yml:generic-api-key:41
|
||||
ba4daa35977d3c7aaecc1f9dd42a6dc41794d04c:group_vars/xworkmate_bridge_distributed.yml:generic-api-key:35
|
||||
126a19e2828f52b2a510e107ef66a9ef1d1e88cf:docs/tldr-ssh-security.md:hashicorp-tf-password:78
|
||||
876
LICENSE
876
LICENSE
@ -1,674 +1,202 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018-2026 Ruohang Feng, @Vonng (rh@vonng.com)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
115
README.md
115
README.md
@ -1 +1,114 @@
|
||||
# playbooks
|
||||
# playbooks
|
||||
|
||||
## XWorkmate Bridge Distributed VPN
|
||||
|
||||
The bidirectional WireGuard-over-VLESS transport for the two XWorkmate bridge
|
||||
nodes is deployed by:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini vpn-wireguard-over-vless.yml
|
||||
```
|
||||
|
||||
The implementation uses split bridge groups (`xworkmate_bridge` and
|
||||
`cn_xworkmate_bridge`) under `xworkmate_bridge_distributed`, stores private keys
|
||||
and the shared management-side Xray UUID in `https://vault.svc.plus`, and keeps
|
||||
the host's default `xray.service` untouched. The runbook lives in
|
||||
[`roles/vhosts/xworkmate_bridge_distributed_vpn/README.md`](/Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks/roles/vhosts/xworkmate_bridge_distributed_vpn/README.md).
|
||||
|
||||
## Cloud Dev Desktop
|
||||
|
||||
The cloud dev desktop flow lives here as two playbooks:
|
||||
|
||||
1. `bootstrap_cloud_dev_desktop.yml`
|
||||
2. `destroy_cloud_dev_desktop.yml`
|
||||
|
||||
`bootstrap_cloud_dev_desktop.yml` now includes the create/bootstrap/verify sequence in one entry point. The control-plane repo calls these playbooks from `../playbooks`.
|
||||
|
||||
## Traffic Billing Stack
|
||||
|
||||
The traffic billing stack now has a single aggregate playbook:
|
||||
|
||||
`deploy_svc_plus_core_services_stack.yml`
|
||||
|
||||
It orchestrates these existing playbooks in dependency order:
|
||||
|
||||
1. `deploy_billing_service.yml`
|
||||
2. `deploy_xworkmate_bridge_vhosts.yml`
|
||||
3. `deploy_xray_exporter.yml`
|
||||
4. `deploy_agent_svc_plus.yml`
|
||||
5. `deploy_accounts_svc_plus.yml`
|
||||
6. `deploy_stunnel-client.yml`
|
||||
7. `deploy_apisix.yml`
|
||||
8. `deploy_console_svc_plus.yml`
|
||||
|
||||
### Full stack deploy
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
export INTERNAL_SERVICE_TOKEN=...
|
||||
export DATABASE_URL=postgres://...
|
||||
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
|
||||
export STACK_TARGET_HOST=jp_xhttp_contabo_host
|
||||
export console_service_sync_dns=true
|
||||
ansible-playbook -i inventory.ini deploy_svc_plus_core_services_stack.yml
|
||||
```
|
||||
|
||||
`STACK_ENV_FILE=./.env` is optional. Use it when you want the aggregate playbook to read a local `.env` file; GitHub Actions or other CI runners can skip it and pass values with `-e` instead.
|
||||
|
||||
### Deploy to one target host directly
|
||||
|
||||
Use `STACK_TARGET_HOST` to override the stack host groups when you want all services to target the same inventory host. For console-only runs, use Ansible's `-l jp_xhttp_contabo_host` limit instead of a separate host variable, and keep `console_service_sync_dns=true` if you want DNS reconciliation.
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
export STACK_TARGET_HOST=jp_xhttp_contabo_host
|
||||
export INTERNAL_SERVICE_TOKEN=...
|
||||
export DATABASE_URL=postgres://...
|
||||
export FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
|
||||
export console_service_sync_dns=true
|
||||
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
|
||||
```
|
||||
|
||||
### Deploy only selected services
|
||||
|
||||
Use `STACK_SERVICES` with a comma-separated list:
|
||||
|
||||
- `billing-service`
|
||||
- `xworkmate-bridge`
|
||||
- `xray-exporter`
|
||||
- `agent`
|
||||
- `accounts`
|
||||
- `stunnel-client`
|
||||
- `apisix`
|
||||
- `console`
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
export STACK_TARGET_HOST=jp-xhttp-contabo.svc.plus
|
||||
export STACK_SERVICES=xray-exporter,billing-service,agent,xworkmate-bridge
|
||||
export INTERNAL_SERVICE_TOKEN=...
|
||||
export DATABASE_URL=postgres://...
|
||||
ansible-playbook -i inventory.ini -l jp_xhttp_contabo_host deploy_svc_plus_core_services_stack.yml
|
||||
```
|
||||
|
||||
### Notes
|
||||
|
||||
- `accounts` and `console` still use their existing role contracts.
|
||||
- `console` requires `FRONTEND_IMAGE` because the target host only does pull-only compose deployment.
|
||||
- `console` now writes a Caddy fragment named like `<server-name>-<release_id>-<hostname>-<domain>.caddy` instead of managing the Caddy service container itself.
|
||||
- `billing-service` requires `DATABASE_URL`.
|
||||
- `xray-exporter` and `agent` require `INTERNAL_SERVICE_TOKEN`.
|
||||
- `xworkmate-bridge` accepts `XWORKMATE_BRIDGE_HOSTS`, and also follows `STACK_TARGET_HOST` when you want to deploy the whole stack to one host.
|
||||
|
||||
### Deploy console to a specific host and sync DNS
|
||||
|
||||
`deploy_console_svc_plus.yml` now accepts `console_service_sync_dns=true` to rebuild and reconcile DNS records after deployment. For host selection, use Ansible's `-l jp_xhttp_contabo_host` limit.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
ansible-playbook -i inventory.ini deploy_console_svc_plus.yml \
|
||||
-e console_service_sync_dns=true \
|
||||
-e FRONTEND_IMAGE=ghcr.io/x-evor/dashboard:latest
|
||||
```
|
||||
|
||||
15
ansible.cfg
15
ansible.cfg
@ -1,15 +1,19 @@
|
||||
[defaults]
|
||||
allow_world_readable_tmpfiles = True
|
||||
# 常用参数
|
||||
inventory = ./inventory # 默认清单文件路径,可按需改
|
||||
vault_password_file = ~/.vault_password
|
||||
# 默认清单文件路径,可按需改
|
||||
inventory = ./inventory.ini
|
||||
timeout = 10
|
||||
forks = 10
|
||||
poll_interval = 10
|
||||
transport = smart
|
||||
gathering = smart
|
||||
fact_caching = jsonfile
|
||||
fact_caching_connection = /tmp/ansible_facts
|
||||
fact_caching_timeout = 3600
|
||||
|
||||
# 输出配置:推荐 yaml,兼容性最好
|
||||
stdout_callback = yaml
|
||||
# 输出配置:使用 ansible-core 内置 callback,避免在轻量 CI 环境里缺少额外插件
|
||||
stdout_callback = default
|
||||
bin_ansible_callbacks = True
|
||||
callbacks_enabled = profile_tasks,timer
|
||||
|
||||
@ -24,3 +28,6 @@ deprecation_warnings = False
|
||||
cache = True
|
||||
cache_plugin = jsonfile
|
||||
cache_timeout = 3600
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
|
||||
28
api.plist.j2
Normal file
28
api.plist.j2
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>plus.svc.xworkspace.api</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-c</string>
|
||||
<string>
|
||||
source "{{ xworkspace_console_config_dir }}/portal.env"
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
exec {{ xworkspace_console_api_exec }}
|
||||
</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{ xworkspace_console_api_working_dir }}</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/api.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/api.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
289
bootstrap_cloud_dev_desktop.yml
Normal file
289
bootstrap_cloud_dev_desktop.yml
Normal file
@ -0,0 +1,289 @@
|
||||
- name: Normalize cloud dev desktop request
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: cloud_vm_request_validate
|
||||
|
||||
- name: Create cloud dev desktop infrastructure
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
|
||||
vars:
|
||||
cloud_lifecycle_action: create
|
||||
- role: cloud_vm_inventory_emit
|
||||
|
||||
- name: Bootstrap remote cloud dev desktop
|
||||
hosts: cloud_desktop
|
||||
gather_facts: true
|
||||
become: "{{ os_family != 'windows' }}"
|
||||
roles:
|
||||
- role: dev_desktop_common
|
||||
when: os_family != "windows"
|
||||
- role: dev_desktop_windows
|
||||
when: os_family == "windows"
|
||||
- role: dev_desktop_fedora_gnome
|
||||
when: os_family == "fedora-gnome"
|
||||
- role: dev_desktop_debian_kde
|
||||
when: os_family == "debian-kde"
|
||||
|
||||
- name: Verify remote cloud dev desktop
|
||||
hosts: cloud_desktop
|
||||
gather_facts: true
|
||||
become: "{{ os_family != 'windows' }}"
|
||||
tasks:
|
||||
- name: Verify common Linux workspace marker
|
||||
ansible.builtin.stat:
|
||||
path: /opt/cloud-dev-desktop/profile.env
|
||||
register: common_profile_marker
|
||||
when: os_family != "windows"
|
||||
|
||||
- name: Assert Linux profile marker exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- common_profile_marker.stat.exists
|
||||
fail_msg: "Missing /opt/cloud-dev-desktop/profile.env marker on Linux host."
|
||||
when: os_family != "windows"
|
||||
|
||||
- name: Verify Fedora GNOME desktop packages
|
||||
ansible.builtin.command: rpm -q gnome-shell gtk3-devel gtk4-devel glib2-devel clang cmake ninja-build
|
||||
changed_when: false
|
||||
when: os_family == "fedora-gnome"
|
||||
|
||||
- name: Verify Debian KDE desktop packages
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
dpkg-query -W plasma-desktop clang cmake ninja-build >/dev/null
|
||||
if dpkg-query -W qt6-base-dev >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
dpkg-query -W qtbase5-dev >/dev/null
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: false
|
||||
when: os_family == "debian-kde"
|
||||
|
||||
- name: Verify Node.js 22+ on Linux
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
test "$(node --version | sed 's/^v//' | cut -d. -f1)" -ge 22
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become_user: "{{ admin_username }}"
|
||||
changed_when: false
|
||||
when: os_family != "windows"
|
||||
|
||||
- name: Verify Go toolchain on Linux
|
||||
ansible.builtin.command: /usr/local/go/bin/go version
|
||||
become_user: "{{ admin_username }}"
|
||||
changed_when: false
|
||||
when: os_family != "windows"
|
||||
|
||||
- name: Verify Codex CLI on Linux
|
||||
ansible.builtin.command: codex --version
|
||||
become_user: "{{ admin_username }}"
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family != "windows"
|
||||
- toolchains.codex | bool
|
||||
|
||||
- name: Verify Flutter is installed on Linux
|
||||
ansible.builtin.shell: |
|
||||
set -euo pipefail
|
||||
test -x {{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter
|
||||
{{ cloud_dev_desktop_flutter_install_root | default('/opt/flutter') }}/bin/flutter --version
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become_user: "{{ admin_username }}"
|
||||
environment:
|
||||
HOME: "/home/{{ admin_username }}"
|
||||
PUB_CACHE: "/home/{{ admin_username }}/.pub-cache"
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family != "windows"
|
||||
- toolchains.flutter | bool
|
||||
|
||||
- name: Verify Codex CLI on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||
$extraPaths = @(
|
||||
$npmUserBin,
|
||||
'C:\Program Files\nodejs',
|
||||
'C:\tools\flutter\bin',
|
||||
'C:\Program Files\Microsoft VS Code\bin',
|
||||
'C:\ProgramData\chocolatey\bin'
|
||||
) | Where-Object { $_ }
|
||||
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||
$codexCmd = Join-Path $env:APPDATA 'npm\codex.cmd'
|
||||
if (-not (Test-Path $codexCmd)) {
|
||||
throw "Missing Codex CLI launcher at $codexCmd"
|
||||
}
|
||||
$codexCmdLine = '"' + $codexCmd + '" --version'
|
||||
cmd.exe /d /c $codexCmdLine | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Codex CLI version probe failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
changed_when: false
|
||||
when: os_family == "windows"
|
||||
|
||||
- name: Verify Node.js 22+ on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||
$extraPaths = @(
|
||||
$npmUserBin,
|
||||
'C:\Program Files\nodejs',
|
||||
'C:\tools\flutter\bin',
|
||||
'C:\Program Files\Microsoft VS Code\bin',
|
||||
'C:\ProgramData\chocolatey\bin'
|
||||
) | Where-Object { $_ }
|
||||
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||
$nodeMajor = [int]((node --version).Trim().TrimStart('v').Split('.')[0])
|
||||
if ($nodeMajor -lt 22) {
|
||||
throw "Node.js 22+ is required, found $(node --version)"
|
||||
}
|
||||
changed_when: false
|
||||
when: os_family == "windows"
|
||||
|
||||
- name: Verify Flutter on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||
$extraPaths = @(
|
||||
$npmUserBin,
|
||||
'C:\Program Files\nodejs',
|
||||
'C:\tools\flutter\bin',
|
||||
'C:\Program Files\Microsoft VS Code\bin',
|
||||
'C:\ProgramData\chocolatey\bin'
|
||||
) | Where-Object { $_ }
|
||||
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||
flutter --version | Out-Null
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family == "windows"
|
||||
- toolchains.flutter | bool
|
||||
|
||||
- name: Verify VS Code on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||
$extraPaths = @(
|
||||
$npmUserBin,
|
||||
'C:\Program Files\nodejs',
|
||||
'C:\tools\flutter\bin',
|
||||
'C:\Program Files\Microsoft VS Code\bin',
|
||||
'C:\ProgramData\chocolatey\bin'
|
||||
) | Where-Object { $_ }
|
||||
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||
Get-Command code | Out-Null
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family == "windows"
|
||||
- toolchains.vscode | bool
|
||||
|
||||
- name: Verify Git on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
|
||||
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
||||
$npmUserBin = if ($env:APPDATA) { Join-Path $env:APPDATA 'npm' } else { '' }
|
||||
$extraPaths = @(
|
||||
$npmUserBin,
|
||||
'C:\Program Files\Git\cmd',
|
||||
'C:\Program Files\nodejs',
|
||||
'C:\tools\flutter\bin',
|
||||
'C:\Program Files\Microsoft VS Code\bin',
|
||||
'C:\ProgramData\chocolatey\bin'
|
||||
) | Where-Object { $_ }
|
||||
$env:Path = (($machinePath, $userPath, ($extraPaths -join ';')) -join ';')
|
||||
git --version | Out-Null
|
||||
changed_when: false
|
||||
when: os_family == "windows"
|
||||
|
||||
- name: Verify Android Studio on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
if (-not (Test-Path 'C:\Program Files\Android\Android Studio\bin\studio64.exe')) {
|
||||
throw 'Missing Android Studio executable at C:\Program Files\Android\Android Studio\bin\studio64.exe'
|
||||
}
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family == "windows"
|
||||
- toolchains.android_studio | bool
|
||||
|
||||
- name: Verify Visual Studio desktop C++ toolchain on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe'
|
||||
if (-not (Test-Path $vswhere)) {
|
||||
throw "Missing vswhere at $vswhere"
|
||||
}
|
||||
$installPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath
|
||||
if (-not $installPath) {
|
||||
throw 'Visual Studio Build Tools with Desktop C++ workload is not installed'
|
||||
}
|
||||
changed_when: false
|
||||
when: os_family == "windows"
|
||||
|
||||
- name: Verify Android SDK and emulator on Windows
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$androidSdkRoot = Join-Path $env:LOCALAPPDATA 'Android\Sdk'
|
||||
$requiredPaths = @(
|
||||
(Join-Path $androidSdkRoot 'platform-tools\adb.exe'),
|
||||
(Join-Path $androidSdkRoot 'emulator\emulator.exe'),
|
||||
(Join-Path $androidSdkRoot 'cmdline-tools\latest\bin\sdkmanager.bat')
|
||||
)
|
||||
foreach ($required in $requiredPaths) {
|
||||
if (-not (Test-Path $required)) {
|
||||
throw "Missing Android SDK component: $required"
|
||||
}
|
||||
}
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family == "windows"
|
||||
- toolchains.android_studio | bool
|
||||
|
||||
- name: Verify Windows Android virtualization features
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$featureNames = @(
|
||||
'Microsoft-Hyper-V-All',
|
||||
'HypervisorPlatform',
|
||||
'VirtualMachinePlatform'
|
||||
)
|
||||
foreach ($featureName in $featureNames) {
|
||||
$feature = Get-WindowsOptionalFeature -Online -FeatureName $featureName
|
||||
if ($feature.State -ne 'Enabled') {
|
||||
throw "Windows optional feature $featureName is not enabled"
|
||||
}
|
||||
}
|
||||
& (Join-Path $env:LOCALAPPDATA 'Android\Sdk\emulator\emulator-check.exe') accel | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Android emulator acceleration probe failed with exit code $LASTEXITCODE"
|
||||
}
|
||||
changed_when: false
|
||||
when:
|
||||
- os_family == "windows"
|
||||
- toolchains.android_studio | bool
|
||||
|
||||
- name: Verify Windows SSHD service
|
||||
ansible.windows.win_shell: |
|
||||
$ErrorActionPreference = "Stop"
|
||||
$sshd = Get-Service sshd
|
||||
if ($sshd.Status -ne 'Running') {
|
||||
throw "SSHD service is not running"
|
||||
}
|
||||
changed_when: false
|
||||
when: os_family == "windows"
|
||||
29
console.plist.j2
Normal file
29
console.plist.j2
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>plus.svc.xworkspace.console</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/bin/bash</string>
|
||||
<string>-c</string>
|
||||
<string>
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
|
||||
# 预编译 runtime 只发 dashboard/dist(无 package.json),且 dashboard 是
|
||||
# 无客户端路由的单页应用,故用 python3 静态伺服 dist 即可(macOS 无 caddy)。
|
||||
exec /usr/bin/env python3 -m http.server {{ xworkspace_console_port }} --bind 127.0.0.1 --directory "{{ xworkspace_console_dashboard_dir }}/dist"
|
||||
</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>{{ xworkspace_console_dashboard_dir }}/dist</string>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/console.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ ansible_env.HOME }}/.local/state/xworkspace/console.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
24
create_audit_user.yml
Normal file
24
create_audit_user.yml
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Create a root-managed SSH audit user on selected hosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
|
||||
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
|
||||
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
|
||||
|
||||
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
|
||||
readonly_ssh_user_profile: audit
|
||||
readonly_ssh_user_lock_password: true
|
||||
readonly_ssh_user_manage_sudoers: true
|
||||
readonly_ssh_user_authorized_keys: >-
|
||||
{{
|
||||
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
|
||||
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
|
||||
else []
|
||||
}}
|
||||
|
||||
roles:
|
||||
- role: readonly_ssh_user
|
||||
25
create_readonly_ssh_user.yml
Normal file
25
create_readonly_ssh_user.yml
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
- name: Create a readonly SSH user on selected hosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
|
||||
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
|
||||
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
|
||||
|
||||
readonly_ssh_user_name: "{{ lookup('env', 'READONLY_SSH_USER_NAME') | default('readonly', true) }}"
|
||||
readonly_ssh_user_profile: "{{ lookup('env', 'READONLY_SSH_USER_PROFILE') | default('readonly', true) }}"
|
||||
readonly_ssh_user_password_hash: "{{ lookup('env', 'READONLY_SSH_USER_PASSWORD_HASH') | default('', true) }}"
|
||||
readonly_ssh_user_lock_password: "{{ lookup('env', 'READONLY_SSH_LOCK_PASSWORD') | default('true', true) | bool }}"
|
||||
readonly_ssh_user_manage_sudoers: "{{ lookup('env', 'READONLY_SSH_ENABLE_SUDO') | default('false', true) | bool }}"
|
||||
readonly_ssh_user_authorized_keys: >-
|
||||
{{
|
||||
[lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY')]
|
||||
if lookup('env', 'READONLY_SSH_USER_PUBLIC_KEY') | default('', true) | length > 0
|
||||
else []
|
||||
}}
|
||||
|
||||
roles:
|
||||
- role: readonly_ssh_user
|
||||
11
deploy_QMD.yml
Normal file
11
deploy_QMD.yml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Deploy QMD extended memory
|
||||
hosts: "{{ qmd_hosts | default('all') }}"
|
||||
become: true
|
||||
gather_facts: true
|
||||
module_defaults:
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
roles:
|
||||
- role: roles/vhosts/qmd/
|
||||
tags: [qmd]
|
||||
26
deploy_accounts_svc_plus.yml
Normal file
26
deploy_accounts_svc_plus.yml
Normal file
@ -0,0 +1,26 @@
|
||||
- name: Deploy managed accounts.svc.plus service
|
||||
hosts: "{{ accounts_service_hosts | default('accounts') }}"
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars:
|
||||
accounts_service_image_ref: >-
|
||||
{{
|
||||
(lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REF') | default('', true) | trim)
|
||||
or
|
||||
(
|
||||
(lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO') | default('ghcr.io/x-evor/accounts', true))
|
||||
~ ':'
|
||||
~ (lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG') | default('latest', true))
|
||||
)
|
||||
}}
|
||||
accounts_service_image_repo: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
|
||||
| default('ghcr.io/x-evor/accounts', true) }}
|
||||
accounts_service_image_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
|
||||
| default('70c6a3f8', true) }}
|
||||
accounts_service_pull_image: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_PULL_IMAGE')
|
||||
| default(false, true) | bool }}
|
||||
roles:
|
||||
- roles/vhosts/accounts_service
|
||||
14
deploy_acp_codex_vhosts.yml
Normal file
14
deploy_acp_codex_vhosts.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Deploy ACP Codex vhosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: roles/vhosts/acp_server_codex/
|
||||
tags: [acp_codex]
|
||||
- role: roles/vhosts/xworkmate_bridge/
|
||||
vars:
|
||||
deploy_acp_codex: true
|
||||
deploy_acp_opencode: false
|
||||
deploy_acp_gemini: false
|
||||
tags: [xworkmate_bridge, acp_codex]
|
||||
14
deploy_acp_gemini_vhosts.yml
Normal file
14
deploy_acp_gemini_vhosts.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Deploy ACP Gemini vhosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: roles/vhosts/acp_server_gemini/
|
||||
tags: [acp_gemini]
|
||||
- role: roles/vhosts/xworkmate_bridge/
|
||||
vars:
|
||||
deploy_acp_codex: false
|
||||
deploy_acp_opencode: false
|
||||
deploy_acp_gemini: true
|
||||
tags: [xworkmate_bridge, acp_gemini]
|
||||
14
deploy_acp_opencode_vhosts.yml
Normal file
14
deploy_acp_opencode_vhosts.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Deploy ACP OpenCode vhosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: roles/vhosts/acp_server_opencode/
|
||||
tags: [acp_opencode]
|
||||
- role: roles/vhosts/xworkmate_bridge/
|
||||
vars:
|
||||
deploy_acp_codex: false
|
||||
deploy_acp_opencode: true
|
||||
deploy_acp_gemini: false
|
||||
tags: [xworkmate_bridge, acp_opencode]
|
||||
9
deploy_agent_hermes.yml
Normal file
9
deploy_agent_hermes.yml
Normal file
@ -0,0 +1,9 @@
|
||||
---
|
||||
- name: Deploy Hermes ACP agent adapter
|
||||
hosts: "{{ acp_hermes_hosts | default('all') }}"
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: roles/vhosts/acp_server_hermes/
|
||||
tags: [acp_hermes, hermes]
|
||||
|
||||
98
deploy_agent_svc_plus.yml
Normal file
98
deploy_agent_svc_plus.yml
Normal file
@ -0,0 +1,98 @@
|
||||
- name: Deploy managed agent.svc.plus service
|
||||
hosts: "{{ agent_service_hosts | default('agent_svc_plus') }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
vars:
|
||||
agent_svc_plus_repo_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_REPO_URL')
|
||||
| default('https://github.com/x-evor/agent.svc.plus.git', true) }}
|
||||
agent_svc_plus_repo_version: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||
| default('main', true) }}
|
||||
agent_svc_plus_release_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_RELEASE_TAG')
|
||||
| default(
|
||||
(lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||
| default('main', true))
|
||||
if ((lookup('ansible.builtin.env', 'AGENT_REPO_VERSION')
|
||||
| default('main', true)) is match('^v.+'))
|
||||
else '',
|
||||
true
|
||||
) }}
|
||||
agent_svc_plus_binary_src: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_BINARY_SRC')
|
||||
| default('', true) }}
|
||||
agent_svc_plus_app_dir: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_APP_DIR')
|
||||
| default('/opt/agent.svc.plus', true) }}
|
||||
agent_svc_plus_go_version: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_GO_VERSION')
|
||||
| default('1.25.1', true) }}
|
||||
agent_id: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_ID')
|
||||
| default('node-xhttp.svc.plus', true) }}
|
||||
agent_tls_cert_name: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_TLS_CERT_NAME')
|
||||
| default(agent_id, true) }}
|
||||
agent_controller_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_CONTROLLER_URL')
|
||||
| default('https://accounts.svc.plus', true) }}
|
||||
agent_api_token: >-
|
||||
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
|
||||
agent_billing_enabled: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_ENABLED')
|
||||
| default(true, true) | bool }}
|
||||
agent_billing_base_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
|
||||
| default('http://127.0.0.1:8081', true) }}
|
||||
agent_billing_http_timeout: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_HTTP_TIMEOUT')
|
||||
| default('15s', true) }}
|
||||
agent_billing_collect_interval: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_COLLECT_INTERVAL')
|
||||
| default('1m', true) }}
|
||||
agent_billing_reconcile_interval: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_BILLING_RECONCILE_INTERVAL')
|
||||
| default('5m', true) }}
|
||||
xray_enabled: >-
|
||||
{{ lookup('ansible.builtin.env', 'AGENT_XRAY_ENABLED')
|
||||
| default(true, true) | bool }}
|
||||
xray_uuid: >-
|
||||
{{ lookup('ansible.builtin.env', 'XRAY_UUID')
|
||||
| default('00000000-0000-0000-0000-000000000000', true) }}
|
||||
pre_tasks:
|
||||
- name: Validate INTERNAL_SERVICE_TOKEN is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- agent_api_token | length > 0
|
||||
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
|
||||
success_msg: "INTERNAL_SERVICE_TOKEN found"
|
||||
|
||||
- name: Gather service facts
|
||||
ansible.builtin.service_facts:
|
||||
|
||||
- name: Assert host is bootstrapped with setup-proxy.sh services
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'xray.service' in ansible_facts.services"
|
||||
- "'xray-tcp.service' in ansible_facts.services"
|
||||
- "'caddy.service' in ansible_facts.services"
|
||||
fail_msg: "Target host must already be bootstrapped by setup-proxy.sh (missing xray.service, xray-tcp.service, or caddy.service)."
|
||||
success_msg: "Target host already has the setup-proxy.sh service layout."
|
||||
|
||||
- name: Assert setup-proxy.sh config paths exist
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
loop:
|
||||
- /etc/caddy/Caddyfile
|
||||
- /usr/local/etc/xray/templates
|
||||
register: agent_bootstrap_paths
|
||||
|
||||
- name: Validate setup-proxy.sh config paths are present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- agent_bootstrap_paths.results | map(attribute='stat.exists') | min
|
||||
fail_msg: "Target host is missing /etc/caddy/Caddyfile or /usr/local/etc/xray/templates. Run setup-proxy.sh first."
|
||||
success_msg: "setup-proxy.sh config paths exist."
|
||||
roles:
|
||||
- roles/vhosts/agent-svc-plus
|
||||
2
deploy_apisix.yml
Normal file
2
deploy_apisix.yml
Normal file
@ -0,0 +1,2 @@
|
||||
---
|
||||
- import_playbook: deploy_apisix_svc.plus.yaml
|
||||
7
deploy_apisix_svc.plus.yaml
Normal file
7
deploy_apisix_svc.plus.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Deploy managed api.svc.plus service
|
||||
hosts: "{{ apisix_service_hosts | default('apisix') }}"
|
||||
gather_facts: false
|
||||
become: true
|
||||
roles:
|
||||
- roles/vhosts/apisix_service
|
||||
87
deploy_billing_service.yml
Normal file
87
deploy_billing_service.yml
Normal file
@ -0,0 +1,87 @@
|
||||
- name: Deploy billing-service
|
||||
hosts: "{{ billing_service_hosts | default('billing_service') }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
vars:
|
||||
billing_service_binary_artifact: >-
|
||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BINARY_ARTIFACT')
|
||||
| default('', true) }}
|
||||
billing_service_image_ref: >-
|
||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_IMAGE_REF')
|
||||
| default('', true) }}
|
||||
billing_service_exporter_base_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
|
||||
| default('http://127.0.0.1:8080', true) }}
|
||||
billing_service_exporter_sources_json: >-
|
||||
{{ lookup('ansible.builtin.env', 'EXPORTER_SOURCES_JSON')
|
||||
| default('', true) }}
|
||||
billing_service_internal_service_token: >-
|
||||
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
|
||||
billing_service_database_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'DATABASE_URL')
|
||||
| default('', true) }}
|
||||
billing_service_listen_addr: >-
|
||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_LISTEN_ADDR')
|
||||
| default('127.0.0.1:8081', true) }}
|
||||
billing_service_collect_interval: >-
|
||||
{{ lookup('ansible.builtin.env', 'COLLECT_INTERVAL')
|
||||
| default('1m', true) }}
|
||||
billing_service_default_region: >-
|
||||
{{ lookup('ansible.builtin.env', 'DEFAULT_REGION')
|
||||
| default('', true) }}
|
||||
billing_service_source_revision: >-
|
||||
{{ lookup('ansible.builtin.env', 'SOURCE_REVISION')
|
||||
| default('billing-service-v1', true) }}
|
||||
billing_service_price_per_byte: >-
|
||||
{{ lookup('ansible.builtin.env', 'PRICE_PER_BYTE')
|
||||
| default('0', true) }}
|
||||
billing_service_initial_included_quota_bytes: >-
|
||||
{{ lookup('ansible.builtin.env', 'INITIAL_INCLUDED_QUOTA_BYTES')
|
||||
| default('0', true) }}
|
||||
billing_service_initial_balance: >-
|
||||
{{ lookup('ansible.builtin.env', 'INITIAL_BALANCE')
|
||||
| default('0', true) }}
|
||||
pre_tasks:
|
||||
- name: Validate BILLING_SERVICE_BINARY_ARTIFACT is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- billing_service_binary_artifact | length > 0
|
||||
fail_msg: "BILLING_SERVICE_BINARY_ARTIFACT must be exported before running this playbook."
|
||||
success_msg: "BILLING_SERVICE_BINARY_ARTIFACT found"
|
||||
- name: Validate BILLING_SERVICE_BINARY_ARTIFACT exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ billing_service_binary_artifact }}"
|
||||
register: billing_service_binary_artifact_stat
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
- name: Assert BILLING_SERVICE_BINARY_ARTIFACT exists on controller
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- billing_service_binary_artifact_stat.stat.exists
|
||||
- billing_service_binary_artifact_stat.stat.isreg
|
||||
fail_msg: "BILLING_SERVICE_BINARY_ARTIFACT must point to an existing binary artifact."
|
||||
success_msg: "BILLING_SERVICE_BINARY_ARTIFACT exists"
|
||||
delegate_to: localhost
|
||||
become: false
|
||||
run_once: true
|
||||
- name: Validate BILLING_SERVICE_IMAGE_REF is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- billing_service_image_ref | length > 0
|
||||
fail_msg: "BILLING_SERVICE_IMAGE_REF must be exported before running this playbook."
|
||||
success_msg: "BILLING_SERVICE_IMAGE_REF found"
|
||||
- name: Validate DATABASE_URL is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- billing_service_database_url | length > 0
|
||||
fail_msg: "DATABASE_URL must be exported before running this playbook."
|
||||
success_msg: "DATABASE_URL found"
|
||||
- name: Validate INTERNAL_SERVICE_TOKEN is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- billing_service_internal_service_token | length > 0
|
||||
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
|
||||
success_msg: "INTERNAL_SERVICE_TOKEN found"
|
||||
roles:
|
||||
- roles/vhosts/billing-service
|
||||
21
deploy_console_svc_plus.yml
Normal file
21
deploy_console_svc_plus.yml
Normal file
@ -0,0 +1,21 @@
|
||||
- name: Deploy managed console.svc.plus service
|
||||
hosts: "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
roles:
|
||||
- roles/vhosts/docker
|
||||
- roles/vhosts/caddy
|
||||
- roles/vhosts/console_service
|
||||
|
||||
- name: Sync console DNS records when requested
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Reconcile Cloudflare DNS for console target host
|
||||
when: console_service_sync_dns | default(false)
|
||||
ansible.builtin.include_role:
|
||||
name: cloudflare_svc_plus_dns
|
||||
vars:
|
||||
cloudflare_dns_source_hosts:
|
||||
- "{{ console_service_target_host | default(console_service_hosts | default('jp_xhttp_contabo_host')) }}"
|
||||
58
deploy_docs_svc_plus.yml
Normal file
58
deploy_docs_svc_plus.yml
Normal file
@ -0,0 +1,58 @@
|
||||
- name: Deploy managed docs.svc.plus service
|
||||
hosts: "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
vars:
|
||||
docs_service_image_ref: >-
|
||||
{{
|
||||
(lookup('ansible.builtin.env', 'DOCS_IMAGE_REF') | default('', true) | trim)
|
||||
or
|
||||
(
|
||||
(lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO') | default('ghcr.io/x-evor/docs', true))
|
||||
~ ':'
|
||||
~ (lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG') | default('latest', true))
|
||||
)
|
||||
}}
|
||||
docs_service_image_repo: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_IMAGE_REPO')
|
||||
| default('ghcr.io/x-evor/docs', true) }}
|
||||
docs_service_image_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_IMAGE_TAG')
|
||||
| default('latest', true) }}
|
||||
docs_service_pull_image: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_PULL_IMAGE')
|
||||
| default(true, true) | bool }}
|
||||
docs_service_knowledge_repo_path_host: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_KNOWLEDGE_REPO_PATH_HOST')
|
||||
| default('', true) }}
|
||||
docs_service_internal_service_token: >-
|
||||
{{
|
||||
lookup('ansible.builtin.env', 'DOCS_INTERNAL_SERVICE_TOKEN')
|
||||
| default(lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true), true)
|
||||
}}
|
||||
docs_service_reload_interval: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_RELOAD_INTERVAL')
|
||||
| default('5m', true) }}
|
||||
docs_service_container_port: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_SERVICE_PORT')
|
||||
| default('8084', true) }}
|
||||
docs_service_host_port: >-
|
||||
{{ lookup('ansible.builtin.env', 'DOCS_HOST_PORT')
|
||||
| default('18086', true) }}
|
||||
roles:
|
||||
- roles/vhosts/docker
|
||||
- roles/vhosts/caddy
|
||||
- roles/vhosts/docs_service
|
||||
|
||||
- name: Sync docs DNS records when requested
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Reconcile Cloudflare DNS for docs target host
|
||||
when: docs_service_sync_dns | default(false)
|
||||
ansible.builtin.include_role:
|
||||
name: cloudflare_svc_plus_dns
|
||||
vars:
|
||||
cloudflare_dns_source_hosts:
|
||||
- "{{ docs_service_target_host | default(docs_service_hosts | default('docs')) }}"
|
||||
11
deploy_gateway_openclaw.yml
Normal file
11
deploy_gateway_openclaw.yml
Normal file
@ -0,0 +1,11 @@
|
||||
---
|
||||
- name: Deploy OpenClaw gateway vhost
|
||||
hosts: "{{ gateway_openclaw_hosts | default('all') }}"
|
||||
become: true
|
||||
gather_facts: true
|
||||
module_defaults:
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
roles:
|
||||
- role: roles/vhosts/gateway_openclaw/
|
||||
tags: [gateway_openclaw, openclaw]
|
||||
10
deploy_modern_it_history.yml
Normal file
10
deploy_modern_it_history.yml
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
- name: Deploy Modern IT History Docusaurus ebook
|
||||
hosts: "{{ modern_it_history_target_host | default('jp_xhttp_contabo_host') }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
vars:
|
||||
nodejs_version: "24.x"
|
||||
roles:
|
||||
- roles/vhosts/nodejs
|
||||
- roles/vhosts/modern_it_history
|
||||
@ -5,8 +5,8 @@
|
||||
become: yes
|
||||
vars:
|
||||
# Choose Node.js version
|
||||
# Examples: "20.x" (LTS), "18.x", "22.x", or specific version like "20.11.0"
|
||||
nodejs_version: "20.x"
|
||||
# Examples: "22.x" (LTS), "20.x", "18.x", or specific version like "20.11.0"
|
||||
nodejs_version: "22.x"
|
||||
|
||||
# Install Yarn package manager (default: true)
|
||||
# install_yarn: false
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
- name: Deploy PostgreSQL on vhosts
|
||||
hosts: "{{ postgresql_target | default('postgresql') }}"
|
||||
become: true
|
||||
vars:
|
||||
group: "{{ group | default(postgresql_target | default('postgresql')) }}"
|
||||
roles:
|
||||
- roles/vhosts/common/
|
||||
- roles/vhosts/postgres/
|
||||
25
deploy_postgresql_svc_plus.yml
Normal file
25
deploy_postgresql_svc_plus.yml
Normal file
@ -0,0 +1,25 @@
|
||||
- name: Deploy managed postgresql.svc.plus service
|
||||
hosts: "{{ postgresql_service_hosts | default('postgresql') }}"
|
||||
gather_facts: false
|
||||
become: true
|
||||
vars:
|
||||
postgresql_service_postgres_image_repo: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_REPO')
|
||||
| default('postgres-extensions', true) }}
|
||||
postgresql_service_postgres_image_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_IMAGE_TAG')
|
||||
| default('17', true) }}
|
||||
postgresql_service_postgres_pull_image: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_POSTGRES_PULL_IMAGE')
|
||||
| default(false, true) | bool }}
|
||||
postgresql_service_stunnel_image_repo: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_REPO')
|
||||
| default('ghcr.io/x-evor/stunnel-server', true) }}
|
||||
postgresql_service_stunnel_image_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_IMAGE_TAG')
|
||||
| default('2330d36', true) }}
|
||||
postgresql_service_stunnel_pull_image: >-
|
||||
{{ lookup('ansible.builtin.env', 'POSTGRESQL_STUNNEL_PULL_IMAGE')
|
||||
| default(false, true) | bool }}
|
||||
roles:
|
||||
- roles/vhosts/postgresql_service
|
||||
20
deploy_stunnel-client.yml
Normal file
20
deploy_stunnel-client.yml
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
- name: Validate shared stunnel-client release entrypoint
|
||||
hosts: "{{ stunnel_client_hosts | default(lookup('ansible.builtin.env', 'STACK_TARGET_HOST') | default('jp_xhttp_contabo_host', true), true) }}"
|
||||
become: true
|
||||
gather_facts: false
|
||||
tasks:
|
||||
- name: Acknowledge shared stunnel-client dry run
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
stunnel-client deployment is not modelled in this repo yet; this
|
||||
placeholder keeps aggregate dry runs green while apply stays blocked.
|
||||
changed_when: false
|
||||
when: ansible_check_mode
|
||||
|
||||
- name: Block apply until shared stunnel-client role exists
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
stunnel-client apply is not implemented in the playbooks repo yet.
|
||||
Add a real stunnel-client role before using this entrypoint in apply mode.
|
||||
when: not ansible_check_mode
|
||||
218
deploy_svc_plus_core_services_stack.yml
Normal file
218
deploy_svc_plus_core_services_stack.yml
Normal file
@ -0,0 +1,218 @@
|
||||
- name: Load stack environment values from local .env file
|
||||
hosts: all
|
||||
gather_facts: false
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
tasks:
|
||||
- name: Parse stack .env file into a dictionary
|
||||
run_once: true
|
||||
delegate_to: localhost
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- python3
|
||||
- -c
|
||||
- |
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import sys
|
||||
|
||||
path = sys.argv[1]
|
||||
data = {}
|
||||
|
||||
if os.path.exists(path):
|
||||
with open(path, encoding="utf-8", errors="ignore") as handle:
|
||||
for raw_line in handle:
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
match = re.match(r"^(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$", line)
|
||||
if not match:
|
||||
continue
|
||||
key, value = match.groups()
|
||||
value = value.strip()
|
||||
if value:
|
||||
try:
|
||||
parts = shlex.split(value, comments=False, posix=True)
|
||||
value = parts[0] if parts else ""
|
||||
except ValueError:
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in ("'", '"'):
|
||||
value = value[1:-1]
|
||||
data[key] = value
|
||||
|
||||
print(json.dumps(data))
|
||||
- "{{ stack_env_file }}"
|
||||
register: stack_env_parse_result
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Store parsed stack environment values
|
||||
run_once: true
|
||||
delegate_to: localhost
|
||||
delegate_facts: true
|
||||
ansible.builtin.set_fact:
|
||||
stack_env_map: "{{ stack_env_parse_result.stdout | default('{}', true) | from_json }}"
|
||||
|
||||
- import_playbook: deploy_billing_service.yml
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
billing_service_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'BILLING_SERVICE_HOSTS')
|
||||
| default('billing_service', true), true) }}
|
||||
billing_service_database_url: >-
|
||||
{{
|
||||
lookup('ansible.builtin.env', 'DATABASE_URL')
|
||||
| default(hostvars['localhost'].stack_env_map.DATABASE_URL
|
||||
| default('', true), true)
|
||||
or (
|
||||
'postgres://%s:%s@%s:%s/%s?sslmode=disable'
|
||||
| format(
|
||||
lookup('ansible.builtin.env', 'POSTGRES_USER')
|
||||
| default(hostvars['localhost'].stack_env_map.POSTGRES_USER
|
||||
| default('svcplus_vps', true), true),
|
||||
lookup('ansible.builtin.env', 'POSTGRES_PASSWORD')
|
||||
| default(hostvars['localhost'].stack_env_map.POSTGRES_PASSWORD
|
||||
| default('', true), true),
|
||||
lookup('ansible.builtin.env', 'BILLING_DB_HOST')
|
||||
| default(hostvars['localhost'].stack_env_map.BILLING_DB_HOST
|
||||
| default('stunnel-client', true), true),
|
||||
lookup('ansible.builtin.env', 'BILLING_DB_PORT')
|
||||
| default(hostvars['localhost'].stack_env_map.BILLING_DB_PORT
|
||||
| default('15432', true), true),
|
||||
lookup('ansible.builtin.env', 'BILLING_DB_NAME')
|
||||
| default(hostvars['localhost'].stack_env_map.BILLING_DB_NAME
|
||||
| default('account', true), true)
|
||||
)
|
||||
)
|
||||
}}
|
||||
billing_service_exporter_base_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'EXPORTER_BASE_URL')
|
||||
| default(hostvars['localhost'].stack_env_map.EXPORTER_BASE_URL
|
||||
| default('http://127.0.0.1:8080', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'billing-service' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_xworkmate_bridge_vhosts.yml
|
||||
vars:
|
||||
xworkmate_bridge_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'XWORKMATE_BRIDGE_HOSTS')
|
||||
| default('all', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'xworkmate-bridge' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_xray_exporter.yml
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
xray_exporter_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'XRAY_EXPORTER_HOSTS')
|
||||
| default('xray_exporter', true), true) }}
|
||||
xray_exporter_internal_service_token: >-
|
||||
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
||||
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
|
||||
| default('', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'xray-exporter' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_agent_svc_plus.yml
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
agent_service_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'AGENT_SERVICE_HOSTS')
|
||||
| default('agent_svc_plus', true), true) }}
|
||||
agent_api_token: >-
|
||||
{{ lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN')
|
||||
| default(hostvars['localhost'].stack_env_map.INTERNAL_SERVICE_TOKEN
|
||||
| default('', true), true) }}
|
||||
agent_billing_base_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'BILLING_SERVICE_BASE_URL')
|
||||
| default(hostvars['localhost'].stack_env_map.BILLING_SERVICE_BASE_URL
|
||||
| default('http://127.0.0.1:8081', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'agent' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_accounts_svc_plus.yml
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
accounts_service_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'ACCOUNTS_SERVICE_HOSTS')
|
||||
| default('accounts', true), true) }}
|
||||
accounts_service_image_repo: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_REPO')
|
||||
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_REPO
|
||||
| default('ghcr.io/x-evor/accounts', true), true) }}
|
||||
accounts_service_image_tag: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_IMAGE_TAG')
|
||||
| default(hostvars['localhost'].stack_env_map.ACCOUNTS_IMAGE_TAG
|
||||
| default('latest', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'accounts' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_stunnel-client.yml
|
||||
vars:
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'stunnel-client' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_apisix.yml
|
||||
vars:
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'apisix' in (stack_services.split(',') | map('trim') | list)"
|
||||
|
||||
- import_playbook: deploy_console_svc_plus.yml
|
||||
vars:
|
||||
stack_env_file: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_ENV_FILE')
|
||||
| default(playbook_dir ~ '/.env', true) }}
|
||||
console_service_hosts: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_TARGET_HOST')
|
||||
| default(lookup('ansible.builtin.env', 'CONSOLE_SERVICE_HOSTS')
|
||||
| default('console', true), true) }}
|
||||
console_service_sync_dns: >-
|
||||
{{ lookup('ansible.builtin.env', 'console_service_sync_dns')
|
||||
| default(lookup('ansible.builtin.env', 'CONSOLE_SERVICE_SYNC_DNS')
|
||||
| default(false, true), true) | bool }}
|
||||
console_service_frontend_image: >-
|
||||
{{ lookup('ansible.builtin.env', 'FRONTEND_IMAGE')
|
||||
| default(hostvars['localhost'].stack_env_map.FRONTEND_IMAGE
|
||||
| default('', true), true) }}
|
||||
console_service_registry_username: >-
|
||||
{{ lookup('ansible.builtin.env', 'GHCR_USERNAME')
|
||||
| default(hostvars['localhost'].stack_env_map.GHCR_USERNAME
|
||||
| default('', true), true) }}
|
||||
console_service_registry_password: >-
|
||||
{{ lookup('ansible.builtin.env', 'GHCR_PASSWORD')
|
||||
| default(hostvars['localhost'].stack_env_map.GHCR_PASSWORD
|
||||
| default('', true), true) }}
|
||||
stack_services: >-
|
||||
{{ lookup('ansible.builtin.env', 'STACK_SERVICES')
|
||||
| default('billing-service,xworkmate-bridge,xray-exporter,agent,accounts,stunnel-client,apisix,console', true) }}
|
||||
when: "'console' in (stack_services.split(',') | map('trim') | list)"
|
||||
33
deploy_svc_plus_extended-services.yml
Normal file
33
deploy_svc_plus_extended-services.yml
Normal file
@ -0,0 +1,33 @@
|
||||
- name: Validate extended services target host for svc.plus
|
||||
hosts: "{{ extended_services_hosts | default('jp_xhttp_contabo_host') }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
tasks:
|
||||
- name: Gather service facts
|
||||
ansible.builtin.service_facts:
|
||||
|
||||
- name: Confirm the host already exposes the shared svc.plus runtime
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'caddy.service' in ansible_facts.services"
|
||||
fail_msg: "Target host must already provide the shared svc.plus runtime (missing caddy.service)."
|
||||
success_msg: "Target host already provides the shared svc.plus runtime."
|
||||
|
||||
- name: Check caddy configuration path
|
||||
ansible.builtin.stat:
|
||||
path: /etc/caddy/Caddyfile
|
||||
register: extended_services_caddyfile
|
||||
|
||||
- name: Validate caddy configuration path exists
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- extended_services_caddyfile.stat.exists
|
||||
fail_msg: "Target host is missing /etc/caddy/Caddyfile."
|
||||
success_msg: "Target host has /etc/caddy/Caddyfile."
|
||||
|
||||
- name: Show extended-services dry-run scope
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Extended services dry-run entrypoint is wired correctly."
|
||||
- "Target host: {{ inventory_hostname }}"
|
||||
- "This playbook currently validates the shared runtime prerequisites used by svc.plus extended services."
|
||||
46
deploy_xray_exporter.yml
Normal file
46
deploy_xray_exporter.yml
Normal file
@ -0,0 +1,46 @@
|
||||
- name: Deploy xray-exporter service
|
||||
hosts: "{{ xray_exporter_hosts | default('xray_exporter') }}"
|
||||
gather_facts: true
|
||||
become: true
|
||||
vars:
|
||||
xray_exporter_source_dir: >-
|
||||
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_SOURCE_DIR')
|
||||
| default(playbook_dir ~ '/../xray-exporter', true) }}
|
||||
xray_exporter_node_id: >-
|
||||
{{ lookup('ansible.builtin.env', 'EXPORTER_NODE_ID')
|
||||
| default(xray_exporter_node_id_custom | default(inventory_hostname, true), true) }}
|
||||
xray_exporter_env_name: >-
|
||||
{{ lookup('ansible.builtin.env', 'EXPORTER_ENV')
|
||||
| default('prod', true) }}
|
||||
xray_exporter_stats_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'XRAY_STATS_URL')
|
||||
| default('http://127.0.0.1:49227/debug/vars', true) }}
|
||||
xray_exporter_stats_token: >-
|
||||
{{ lookup('ansible.builtin.env', 'XRAY_STATS_TOKEN')
|
||||
| default('', true) }}
|
||||
xray_exporter_accounts_base_url: >-
|
||||
{{ lookup('ansible.builtin.env', 'ACCOUNTS_BASE_URL')
|
||||
| default('https://accounts.svc.plus', true) }}
|
||||
xray_exporter_internal_service_token: >-
|
||||
{{ lookup('ansible.builtin.vars', 'INTERNAL_SERVICE_TOKEN', default=lookup('ansible.builtin.env', 'INTERNAL_SERVICE_TOKEN') | default('', true)) }}
|
||||
xray_exporter_snapshot_store_path: >-
|
||||
{{ lookup('ansible.builtin.env', 'SNAPSHOT_STORE_PATH')
|
||||
| default('/var/lib/xray-exporter/snapshots.db', true) }}
|
||||
xray_exporter_snapshot_retention: >-
|
||||
{{ lookup('ansible.builtin.env', 'SNAPSHOT_RETENTION')
|
||||
| default('72h', true) }}
|
||||
xray_exporter_listen_addr: >-
|
||||
{{ lookup('ansible.builtin.env', 'XRAY_EXPORTER_LISTEN_ADDR')
|
||||
| default('127.0.0.1:8080', true) }}
|
||||
xray_exporter_scrape_interval: >-
|
||||
{{ lookup('ansible.builtin.env', 'SCRAPE_INTERVAL')
|
||||
| default('1m', true) }}
|
||||
pre_tasks:
|
||||
- name: Validate INTERNAL_SERVICE_TOKEN is present
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- xray_exporter_internal_service_token | length > 0
|
||||
fail_msg: "INTERNAL_SERVICE_TOKEN must be exported before running this playbook."
|
||||
success_msg: "INTERNAL_SERVICE_TOKEN found"
|
||||
roles:
|
||||
- roles/vhosts/xray-exporter
|
||||
19
deploy_xworkmate_bridge_vhosts.yml
Normal file
19
deploy_xworkmate_bridge_vhosts.yml
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
- name: Deploy ACP vhosts through xworkmate bridge
|
||||
hosts: "{{ xworkmate_bridge_hosts | default('all') }}"
|
||||
become: true
|
||||
gather_facts: true
|
||||
module_defaults:
|
||||
ansible.builtin.apt:
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
roles:
|
||||
- role: roles/vhosts/acp_server_codex/
|
||||
tags: [acp_codex]
|
||||
- role: roles/vhosts/acp_server_opencode/
|
||||
tags: [acp_opencode]
|
||||
- role: roles/vhosts/acp_server_gemini/
|
||||
tags: [acp_gemini]
|
||||
- role: roles/vhosts/acp_server_hermes/
|
||||
tags: [acp_hermes]
|
||||
- role: roles/vhosts/xworkmate_bridge/
|
||||
tags: [xworkmate_bridge]
|
||||
9
destroy_cloud_dev_desktop.yml
Normal file
9
destroy_cloud_dev_desktop.yml
Normal file
@ -0,0 +1,9 @@
|
||||
- name: Destroy cloud dev desktop infrastructure
|
||||
hosts: localhost
|
||||
connection: local
|
||||
gather_facts: true
|
||||
roles:
|
||||
- role: cloud_vm_request_validate
|
||||
- role: "{{ (provider == 'azure') | ternary('azure_dev_desktop_lifecycle', 'gcp_dev_desktop_lifecycle') }}"
|
||||
vars:
|
||||
cloud_lifecycle_action: destroy
|
||||
170
docs/ai-workspace-runtime-delivery-plan.md
Normal file
170
docs/ai-workspace-runtime-delivery-plan.md
Normal file
@ -0,0 +1,170 @@
|
||||
# AI Workspace Runtime 交付计划
|
||||
|
||||
## 1. 目标与边界
|
||||
|
||||
本计划定义 AI Workspace 核心运行时从源码仓库构建、发布、离线聚合到目标机部署的完整交付链路。
|
||||
|
||||
核心原则:
|
||||
|
||||
- LiteLLM、xworkspace-console、xworkmate-bridge、QMD 分别在各自源码仓库的 GitHub Actions build job 中构建。
|
||||
- 每个组件独立发布 `runtime-*` GitHub Release 及其 SHA256 清单。
|
||||
- offline package 只下载已发布产物,逐文件完成 SHA256 校验后再聚合。
|
||||
- 目标机只允许校验、解包、安装、配置、启动和健康检查,禁止源码编译、依赖构建及镜像构建。
|
||||
- 所有未经过 CI 或目标机矩阵实测的能力均保持 `TODO`,不得仅依据设计或局部实现标记完成。
|
||||
|
||||
## 2. 目标架构
|
||||
|
||||
```text
|
||||
LiteLLM repository ---------- build job --> runtime-litellm-* ----------\
|
||||
xworkspace-console repository build job --> runtime-xworkspace-console-* --\
|
||||
xworkmate-bridge repository -- build job --> runtime-xworkmate-bridge-* -----+--> offline package job
|
||||
QMD repository -------------- build job --> runtime-qmd-* ------------------/ |
|
||||
| download
|
||||
| SHA256 verify
|
||||
| manifest aggregate
|
||||
v
|
||||
offline-package-*
|
||||
|
|
||||
v
|
||||
target host: verify/install only
|
||||
```
|
||||
|
||||
### 2.1 组件 Release
|
||||
|
||||
四个组件必须由各自仓库负责构建,聚合仓库不得从源码代建组件。
|
||||
|
||||
| 组件 | 构建责任 | Release 命名 | 必需产物 |
|
||||
| --- | --- | --- | --- |
|
||||
| LiteLLM | LiteLLM 仓库 GitHub Actions build job | `runtime-litellm-*` | 固定版本 Python runtime/依赖包、启动入口、组件 manifest、SHA256 清单 |
|
||||
| xworkspace-console | xworkspace-console 仓库 GitHub Actions build job | `runtime-xworkspace-console-*` | dashboard 静态产物、API 二进制、运行配置模板、组件 manifest、SHA256 清单 |
|
||||
| xworkmate-bridge | xworkmate-bridge 仓库 GitHub Actions build job | `runtime-xworkmate-bridge-*` | bridge 二进制、systemd/运行配置模板、组件 manifest、SHA256 清单 |
|
||||
| QMD | QMD 仓库 GitHub Actions build job | `runtime-qmd-*` | 已安装依赖和已构建 CLI/runtime、组件 manifest、SHA256 清单 |
|
||||
|
||||
资产文件名必须精确匹配,聚合器和目标机均不得尝试别名、模糊匹配或兼容猜测:
|
||||
|
||||
- Console:`xworkspace-console-runtime-linux-{amd64|arm64}.tar.gz`
|
||||
- Bridge:`xworkmate-bridge-linux-{amd64|arm64}.tar.gz`
|
||||
- QMD:`qmd-runtime-linux-{amd64|arm64}.tar.gz`
|
||||
- LiteLLM:`litellm-runtime-{distro}-{version}-{arch}.tar.gz`
|
||||
|
||||
每个组件 manifest 至少记录:组件名、源码 commit、版本、构建时间、目标 OS、目标架构、入口文件、文件列表及每个文件的 SHA256。
|
||||
|
||||
### 2.2 offline package 聚合
|
||||
|
||||
offline package job 必须:
|
||||
|
||||
1. 从四个组件的 `runtime-*` Release 下载与目标平台匹配的产物和 SHA256 清单。
|
||||
2. 在聚合前执行 SHA256 校验;缺少清单、文件缺失或摘要不一致时立即失败。
|
||||
3. 生成聚合 manifest,固定四个组件的 Release tag、源码 commit、资产 URL、资产大小和 SHA256。
|
||||
4. 将已校验组件产物、部署 playbook 所需依赖及聚合 manifest 打包为 `offline-package-*`。
|
||||
5. 对最终 offline package 再生成 SHA256,并在 CI 中执行一次解包与结构校验。
|
||||
|
||||
禁止以 `latest` 作为不可追溯的部署输入;重新聚合必须基于明确 tag 或不可变 commit。
|
||||
|
||||
### 2.3 目标机部署
|
||||
|
||||
目标机部署必须开启 prebuilt-only 约束。缺少任一预构建产物时直接失败,不得回退到以下行为:
|
||||
|
||||
- `git clone` 或源码 checkout;
|
||||
- `npm install`、`npm run build`、`go build`、`go run`;
|
||||
- `pip install` 从公网或源码解析构建依赖;
|
||||
- `docker build`、`podman build` 或其他本地镜像构建;
|
||||
- 任何需要编译器、SDK 或前端构建工具链的安装步骤。
|
||||
|
||||
部署仅执行:offline package SHA256 校验、manifest 校验、解包、文件安装、权限设置、配置渲染、服务启动、健康检查和结果汇总。
|
||||
|
||||
## 3. 资源与性能约束
|
||||
|
||||
### 3.1 并发控制
|
||||
|
||||
- 全局并发硬上限必须满足 `并发数 <= 2 * 在线 CPU 数`,在线 CPU 数以执行时实际可用 CPU 为准。
|
||||
- 初始并发取任务上限、配置上限和 `2 * 在线 CPU 数` 三者最小值。
|
||||
- 调度器必须随 load 动态收缩:负载超过阈值时停止发放新任务并逐级降低并发;负载恢复且持续稳定后再缓慢扩容。
|
||||
- 动态收缩不得中断正在执行的不可重入安装步骤;只限制后续任务进入。
|
||||
- 日志和最终摘要必须记录 CPU 数、load 采样、每次并发调整的时间、原因及调整前后值。
|
||||
|
||||
### 3.2 部署耗时分布
|
||||
|
||||
每次部署必须记录总耗时及至少以下阶段耗时:
|
||||
|
||||
- offline package 下载;
|
||||
- SHA256 与 manifest 校验;
|
||||
- 解包;
|
||||
- 各组件安装;
|
||||
- 配置渲染;
|
||||
- 服务启动;
|
||||
- 健康检查。
|
||||
|
||||
CI/验收报告按 OS、架构、冷启动/缓存命中、首次执行/幂等重跑分组,统计样本数、最小值、最大值、平均值以及 P50、P90、P95、P99。样本不足时保留原始数据并明确标注,不以单次耗时代替分布结论。
|
||||
|
||||
## 4. 支持矩阵与验收
|
||||
|
||||
目标支持以下全部组合:
|
||||
|
||||
| 发行版 | 版本 | 架构 |
|
||||
| --- | --- | --- |
|
||||
| Debian | 11、12、13 | amd64、arm64 |
|
||||
| Ubuntu | 22.04、24.04、26.04 | amd64、arm64 |
|
||||
|
||||
每个矩阵项必须验证:
|
||||
|
||||
1. offline package 下载和 SHA256 校验成功。
|
||||
2. 目标机在无源码、无构建工具链、组件外网访问受限的条件下部署成功。
|
||||
3. 四个组件版本与聚合 manifest 完全一致。
|
||||
4. 服务启动、健康检查和关键 smoke test 成功。
|
||||
5. 同一主机使用同一输入至少连续执行两次;第二次成功且无非预期变更、无重复资源、无凭据轮换、无构建行为。
|
||||
6. 首次部署和幂等重跑均产出阶段耗时及完整摘要。
|
||||
|
||||
Ubuntu 26.04 在实际可用 runner/镜像和依赖生态完成验证前,只能保持计划支持状态,不得标记已验证。
|
||||
|
||||
## 5. 当前事实
|
||||
|
||||
以下状态只记录当前仓库或相邻交付文档能够证明的事实,不把目标设计视为完成:
|
||||
|
||||
- [x] 聚合入口已拆分为 preflight 与 runtime playbook;preflight 已校验 `docker`、`k3s`、`systemd` 运行模式组合。
|
||||
- [x] xworkspace-console 与 QMD 的部署代码已出现预构建 archive 输入及 prebuilt-only 缺包失败入口。
|
||||
- [x] 相邻一键部署文档已记录:xworkspace-console 离线包 `publish-release` 链路和 Release 产物上传曾核对完成。
|
||||
- [x] 相邻一键部署文档已记录:一键安装脚本优先使用离线安装包。
|
||||
- [ ] xworkspace-console 与 QMD 当前仍存在目标机源码 checkout/依赖安装/构建回退,尚未满足“目标机禁止构建”。
|
||||
- [ ] LiteLLM 当前可覆盖 package spec,但未证明其独立 `runtime-litellm-*` Release 和完全离线、免构建安装链路。
|
||||
- [ ] xworkmate-bridge 独立 `runtime-xworkmate-bridge-*` Release 和预构建消费链路尚未在本计划范围内验证。
|
||||
- [ ] 四组件 Release 的一致命名、manifest 和 SHA256 契约尚未完成验证。
|
||||
- [ ] offline package 的逐文件下载、SHA256 校验、聚合 manifest 和最终包校验尚未完成验证。
|
||||
- [ ] 并发硬上限、基于 load 的动态收缩及调整日志尚未完成验证。
|
||||
- [ ] 部署耗时分布统计尚未完成验证。
|
||||
- [ ] 连续重复执行的幂等性验收尚未完成。
|
||||
- [ ] Debian 11/12/13、Ubuntu 22.04/24.04/26.04 的 amd64/arm64 全矩阵尚未完成验证。
|
||||
|
||||
## 6. TODO
|
||||
|
||||
### P0:构建与发布闭环
|
||||
|
||||
- [ ] TODO:在 LiteLLM 仓库建立 build job,发布 `runtime-litellm-*` Release、组件 manifest 和 SHA256 清单。
|
||||
- [ ] TODO:在 xworkspace-console 仓库固化 build job,确认每次发布 `runtime-xworkspace-console-*` Release、组件 manifest 和 SHA256 清单。
|
||||
- [ ] TODO:在 xworkmate-bridge 仓库建立 build job,发布 `runtime-xworkmate-bridge-*` Release、组件 manifest 和 SHA256 清单。
|
||||
- [ ] TODO:在 QMD 仓库建立 build job,发布 `runtime-qmd-*` Release、组件 manifest 和 SHA256 清单。
|
||||
- [ ] TODO:为 amd64、arm64 分别产出可安装资产;若资产与发行版相关,则按支持矩阵拆分并在 manifest 中明确兼容范围。
|
||||
- [ ] TODO:增加 Release 契约测试,拒绝缺失入口、manifest、SHA256 或架构资产的发布。
|
||||
|
||||
### P0:离线聚合与目标机免构建
|
||||
|
||||
- [ ] TODO:实现 offline package job,按固定 tag 下载四组件 Release,并在聚合前逐文件执行 SHA256 校验。
|
||||
- [ ] TODO:生成可追溯聚合 manifest,并为最终 `offline-package-*` 生成和发布 SHA256。
|
||||
- [ ] TODO:在目标机部署入口强制 prebuilt-only,删除或禁用四组件所有源码构建回退。
|
||||
- [ ] TODO:增加“目标机禁止构建”守卫,检测到编译器调用、包构建命令、源码 checkout 或镜像构建即失败。
|
||||
- [ ] TODO:在断网或仅允许访问 offline package 源的目标机上完成端到端部署验证。
|
||||
|
||||
### P1:并发、性能与可观测性
|
||||
|
||||
- [ ] TODO:实现在线 CPU 探测和 `<= 2 * 在线 CPU` 的全局并发硬限制。
|
||||
- [ ] TODO:定义 load 采样窗口、收缩/恢复阈值、迟滞策略和最低并发,完成动态收缩测试。
|
||||
- [ ] TODO:记录阶段级耗时、组件级耗时、并发变化和环境标签,产出结构化 JSON 及人类可读摘要。
|
||||
- [ ] TODO:汇总部署耗时分布,至少输出 count/min/max/avg/P50/P90/P95/P99,并区分首次执行与幂等重跑。
|
||||
|
||||
### P1:幂等与平台矩阵
|
||||
|
||||
- [ ] TODO:为每个支持矩阵项连续执行至少两次,验证第二次无非预期 changed、服务中断、重复资源或凭据变化。
|
||||
- [ ] TODO:覆盖 Debian 11/12/13 amd64/arm64。
|
||||
- [ ] TODO:覆盖 Ubuntu 22.04/24.04/26.04 amd64/arm64。
|
||||
- [ ] TODO:保存每个矩阵项的 Release tag、offline package SHA256、部署日志、耗时数据和验收结论。
|
||||
- [ ] TODO:全部矩阵通过后,再把“计划支持”更新为“已验证支持”;部分通过时逐项记录,不做整体完成声明。
|
||||
77
docs/cert-manager-arch.md
Normal file
77
docs/cert-manager-arch.md
Normal file
@ -0,0 +1,77 @@
|
||||
# cert-manager Architecture
|
||||
|
||||
This document records the complete certificate control-plane contract for the `svc.plus` platform.
|
||||
|
||||
## Scope
|
||||
|
||||
The system is split into four distinct responsibilities:
|
||||
|
||||
- `cert-manager` owns certificate issuance, renewal, and the target `Secret` objects.
|
||||
- `Caddy` remains the ingress surface and serves HTTP-01 challenge traffic.
|
||||
- `external-dns` only manages DNS records for public hostnames.
|
||||
- `external-secrets` continues to materialize Vault-sourced application secrets, AK/SK pairs, future provider credentials such as the Cloudflare API token, and image pull secrets.
|
||||
|
||||
## Default Contract
|
||||
|
||||
- `postgresql-prod.svc.plus` defaults to `cert-manager + ACME HTTP-01`.
|
||||
- `DNS-01 + Cloudflare` is predeclared for wildcard certificates and future subdomains.
|
||||
- `selfSigned` remains available as an internal temporary or recovery fallback.
|
||||
- `cert-manager` owns `postgresql-tls` in every namespace that consumes it, so there is no cross-namespace Secret sync job.
|
||||
|
||||
## System Diagram
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Vault[(Vault)]
|
||||
ESO[external-secrets]
|
||||
CloudflareToken[(cloudflare-api-token Secret)]
|
||||
ExternalDNS[external-dns]
|
||||
DNSZone[(svc.plus DNS zone)]
|
||||
Caddy[Caddy Ingress]
|
||||
CertMgr[cert-manager]
|
||||
Http01["ACME HTTP-01"]
|
||||
Dns01["ACME DNS-01 + Cloudflare"]
|
||||
SelfSigned["selfSigned fallback"]
|
||||
PlatformCert["platform/postgresql-tls Certificate"]
|
||||
PlatformSecret["platform/postgresql-tls Secret"]
|
||||
DatabaseCert["database/postgresql-tls Certificate"]
|
||||
DatabaseSecret["database/postgresql-tls Secret"]
|
||||
PostgreSQL["postgresql-prod.svc.plus"]
|
||||
Stunnel["database/stunnel-server"]
|
||||
|
||||
Vault --> ESO
|
||||
ESO --> CloudflareToken
|
||||
CloudflareToken --> Dns01
|
||||
ExternalDNS --> DNSZone
|
||||
DNSZone --> Caddy
|
||||
Caddy --> Http01
|
||||
Http01 --> CertMgr
|
||||
Dns01 --> CertMgr
|
||||
SelfSigned -. fallback .-> CertMgr
|
||||
CertMgr --> PlatformCert
|
||||
CertMgr --> DatabaseCert
|
||||
PlatformCert --> PlatformSecret
|
||||
DatabaseCert --> DatabaseSecret
|
||||
PlatformSecret --> Caddy
|
||||
DatabaseSecret --> Stunnel
|
||||
Caddy --> PostgreSQL
|
||||
```
|
||||
|
||||
## Operational Rules
|
||||
|
||||
- Keep `cert-manager` as the source of truth for TLS Secret ownership.
|
||||
- Keep `Caddy` as the traffic and HTTP-01 routing layer only.
|
||||
- Keep `external-dns` focused on DNS record reconciliation.
|
||||
- Keep `external-secrets` focused on external secret materialization.
|
||||
- Treat the Cloudflare API token as an external input secret; it can be bootstrapped manually or delivered by `external-secrets` when that path is wired in.
|
||||
- Prefer namespace-local `Certificate` objects for each consumer namespace.
|
||||
- Avoid cross-namespace certificate copying or Secret sync controllers.
|
||||
|
||||
## Related Playbook Roles
|
||||
|
||||
- `vhosts/k3s_platform_bootstrap`
|
||||
- installs the platform node and prepares GitOps handoff
|
||||
- `vhosts/k3s_platform_addon`
|
||||
- installs shared platform services such as `cert-manager`, `external-secrets`, `caddy`, and `external-dns`
|
||||
- `GitOps`
|
||||
- owns the namespace-local `Certificate` manifests and workload wiring
|
||||
258
docs/k3s-role-map.md
Normal file
258
docs/k3s-role-map.md
Normal file
@ -0,0 +1,258 @@
|
||||
# K3S Role Map
|
||||
|
||||
This document defines the recommended reuse path for `playbooks/roles/vhosts/k3s*`.
|
||||
|
||||
## Goal
|
||||
|
||||
The `k3s*` roles are no longer treated as one flat group.
|
||||
|
||||
They are split into:
|
||||
|
||||
- baseline roles used by all nodes
|
||||
- platform bootstrap roles used by GitOps-driven single-node platform deployments
|
||||
- cluster lifecycle roles used by multi-node cluster deployments
|
||||
- frozen legacy roles kept only for compatibility
|
||||
- reset roles kept for teardown and recovery
|
||||
|
||||
## Recommended Mainline
|
||||
|
||||
Recommended path for a new platform project:
|
||||
|
||||
`common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
|
||||
|
||||
### Edge And Certificate System
|
||||
|
||||
The full certificate control-plane contract is documented in
|
||||
[cert-manager-arch.md](./cert-manager-arch.md).
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- `common`
|
||||
- host baseline
|
||||
- package baseline
|
||||
- file limits
|
||||
- shared OS hardening
|
||||
- `k3s_platform_bootstrap`
|
||||
- install single-node k3s
|
||||
- render `/etc/rancher/k3s/config.yaml`
|
||||
- disable built-in components such as `traefik`
|
||||
- install Flux
|
||||
- connect the node to the GitOps repository
|
||||
- `k3s_platform_addon`
|
||||
- install platform-side shared components into Kubernetes
|
||||
- examples: `cert-manager`, `external-secrets`, `reloader`, `caddy`, `apisix`, `external-dns`
|
||||
- certificate control plane, DNS automation, and external secret sync all belong in this layer
|
||||
- see [cert-manager-arch.md](./cert-manager-arch.md) for the full edge and certificate contract
|
||||
- `GitOps`
|
||||
- own dynamic platform configuration
|
||||
- own workload manifests
|
||||
- own service wiring and dependencies
|
||||
|
||||
Entry playbooks:
|
||||
|
||||
- `playbooks/k3s_platform_bootstrap_with_gitops.yml`
|
||||
- `playbooks/k3s_platform_addon.yml`
|
||||
|
||||
Use this path when:
|
||||
|
||||
- the target is a standard platform node
|
||||
- Flux/GitOps is the desired source of truth
|
||||
- platform addons should be declared instead of installed by ad hoc scripts
|
||||
|
||||
## Cluster Mainline
|
||||
|
||||
Recommended path for a new multi-node cluster project:
|
||||
|
||||
`common -> k3s-cluster-server / k3s-cluster-agent`
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- `common`
|
||||
- shared host baseline before cluster work starts
|
||||
- `k3s-cluster-server`
|
||||
- server lifecycle role
|
||||
- dispatches by `action`
|
||||
- current action set includes `bootstrap`, `add-master`, `backup`, `recovery`, `upgrade`, `destroy`
|
||||
- `k3s-cluster-agent`
|
||||
- agent lifecycle role
|
||||
- dispatches by `action`
|
||||
- current action set includes `bootstrap`, `upgrade`, `destroy`
|
||||
|
||||
Entry playbooks:
|
||||
|
||||
- `playbooks/init_k3s_cluster_server`
|
||||
- `playbooks/init_k3s_cluster_agent`
|
||||
|
||||
Use this path when:
|
||||
|
||||
- the target is a server/agent topology
|
||||
- cluster lifecycle needs to be managed explicitly
|
||||
- node roles and actions are different between control plane and workers
|
||||
|
||||
## Role Status
|
||||
|
||||
| Role | Status | Use |
|
||||
| --- | --- | --- |
|
||||
| `vhosts/k3s_platform_bootstrap` | recommended | single-node platform bootstrap |
|
||||
| `vhosts/k3s_platform_addon` | recommended | platform addon installation before GitOps takeover |
|
||||
| `vhosts/k3s-cluster-server` | recommended | cluster server lifecycle |
|
||||
| `vhosts/k3s-cluster-agent` | recommended | cluster agent lifecycle |
|
||||
| `vhosts/k3s-reset` | keep | reset / teardown |
|
||||
| `vhosts/k3s` | frozen | legacy single-node installer |
|
||||
| `vhosts/k3s-cluster` | frozen | legacy script-driven cluster installer |
|
||||
| `vhosts/k3s-addon` | frozen | legacy ingress and DNS addon wrapper |
|
||||
|
||||
## Frozen Role Migration
|
||||
|
||||
The frozen roles are not the recommended extension points anymore.
|
||||
|
||||
### `vhosts/k3s`
|
||||
|
||||
Legacy behavior:
|
||||
|
||||
- runs `setup-k3s.sh`
|
||||
- optionally installs `kubeovn`
|
||||
|
||||
Replace with:
|
||||
|
||||
- `common`
|
||||
- `k3s_platform_bootstrap`
|
||||
|
||||
Notes:
|
||||
|
||||
- `k3s_platform_bootstrap` is the right place to absorb the installation capability from `vhosts/k3s`
|
||||
- the target direction is capability fusion followed by gradual retirement of the old `vhosts/k3s` role
|
||||
- the preferred reuse mode is capability extraction and inward migration, not direct role chaining
|
||||
- shared logic worth absorbing includes installer download, mirror selection, default k3s flags, and helm bootstrap
|
||||
- `kubeovn` and legacy CNI-specific behavior should not be pulled into the new platform bootstrap path by default
|
||||
- new single-node installs should be expressed through rendered k3s config and Flux bootstrap
|
||||
- do not add new platform capabilities to `vhosts/k3s`
|
||||
|
||||
### `vhosts/k3s-cluster`
|
||||
|
||||
Legacy behavior:
|
||||
|
||||
- copies shell scripts to remote hosts
|
||||
- runs `setup_k3s.sh`
|
||||
- runs `set-registry.sh`
|
||||
|
||||
Replace with:
|
||||
|
||||
- `common`
|
||||
- `k3s-cluster-server`
|
||||
- `k3s-cluster-agent`
|
||||
|
||||
Notes:
|
||||
|
||||
- new lifecycle actions should be added to the server or agent action roles
|
||||
- do not extend the script-wrapper model further
|
||||
|
||||
### `vhosts/k3s-addon`
|
||||
|
||||
Legacy behavior:
|
||||
|
||||
- wraps ingress and DNS shell scripts
|
||||
- mixes ingress installation details with addon orchestration
|
||||
|
||||
Replace with:
|
||||
|
||||
- `k3s_platform_addon`
|
||||
- GitOps-managed service configuration
|
||||
|
||||
Notes:
|
||||
|
||||
- new ingress, gateway, DNS, and platform addon logic should move into `k3s_platform_addon`
|
||||
- keep `k3s-addon` only for compatibility with older environments
|
||||
|
||||
## New Project Selection
|
||||
|
||||
For a new project, choose the path by deployment style.
|
||||
|
||||
### Option A: Platform Node With GitOps
|
||||
|
||||
Choose:
|
||||
|
||||
`common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
|
||||
|
||||
Best for:
|
||||
|
||||
- `svc.plus` style platform nodes
|
||||
- single-node or platform-first installations
|
||||
- shared addons managed centrally
|
||||
- GitOps as the long-term control plane
|
||||
|
||||
### Option B: Multi-Node Cluster Lifecycle
|
||||
|
||||
Choose:
|
||||
|
||||
`common -> k3s-cluster-server / k3s-cluster-agent`
|
||||
|
||||
Best for:
|
||||
|
||||
- explicit server/agent topologies
|
||||
- controlled bootstrap, upgrade, backup, and recovery flows
|
||||
- clusters that need role-aware lifecycle operations
|
||||
|
||||
### Option C: Reset Only
|
||||
|
||||
Choose:
|
||||
|
||||
`k3s-reset`
|
||||
|
||||
Best for:
|
||||
|
||||
- teardown
|
||||
- rebuild preparation
|
||||
- cleanup after failed bootstrap
|
||||
|
||||
## Extension Rules
|
||||
|
||||
When adding new capabilities:
|
||||
|
||||
- host-level baseline belongs in `common`
|
||||
- k3s installation and Flux bootstrap belong in `k3s_platform_bootstrap`
|
||||
- platform shared addons belong in `k3s_platform_addon`
|
||||
- TLS issuers and namespace-local certificate lifecycle should also live there
|
||||
- the complete edge and certificate contract lives in [cert-manager-arch.md](./cert-manager-arch.md)
|
||||
- server and agent lifecycle actions belong in `k3s-cluster-server` or `k3s-cluster-agent`
|
||||
- dynamic service configuration belongs in GitOps
|
||||
- reset and cleanup behavior belongs in `k3s-reset`
|
||||
|
||||
GitOps certificate rule of thumb:
|
||||
|
||||
- use `cert-manager` to own the `Certificate` in the namespace that consumes it
|
||||
- avoid cross-namespace Secret sync jobs when the same certificate can be declared directly in each namespace
|
||||
- use `Caddy` only as the ingress / HTTP-01 routing surface for those certificates
|
||||
- use `external-dns` only for DNS record updates
|
||||
- keep `external-secrets` for Vault-sourced app credentials, cloud API keys, and image pull secrets
|
||||
|
||||
Do not add new functionality to:
|
||||
|
||||
- `vhosts/k3s`
|
||||
- `vhosts/k3s-cluster`
|
||||
- `vhosts/k3s-addon`
|
||||
|
||||
## Reuse Guidance
|
||||
|
||||
`vhosts/k3s` is frozen as an external entry role, but its useful install logic should be migrated inward.
|
||||
|
||||
Preferred direction:
|
||||
|
||||
- keep `vhosts/k3s` frozen as a compatibility wrapper
|
||||
- move reusable k3s install logic into `k3s_platform_bootstrap` over time
|
||||
- let `k3s_platform_bootstrap` become the single canonical owner of the platform install path
|
||||
- shrink `vhosts/k3s` as pieces are absorbed until it can be removed safely
|
||||
- treat `k3s_platform_bootstrap` as the canonical owner of the new single-node platform bootstrap path
|
||||
|
||||
Avoid:
|
||||
|
||||
- adding `include_role: vhosts/k3s` inside `k3s_platform_bootstrap`
|
||||
- extending the old script-wrapper interface as the long-term contract
|
||||
|
||||
## Short Decision Rule
|
||||
|
||||
If the question is "which role should a new project reuse?", use:
|
||||
|
||||
- platform project: `common -> k3s_platform_bootstrap -> k3s_platform_addon -> GitOps`
|
||||
- multi-node cluster project: `common -> k3s-cluster-server / k3s-cluster-agent`
|
||||
- cleanup project: `k3s-reset`
|
||||
576
docs/litellm-gateway-deployment.md
Normal file
576
docs/litellm-gateway-deployment.md
Normal file
@ -0,0 +1,576 @@
|
||||
# LiteLLM Gateway 部署指南
|
||||
|
||||
## 目标架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Caddy (HTTPS Entry) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
Internet ──────────►│ │ api.svc.plus/v1/openai/* │ │
|
||||
│ │ api.svc.plus/v1/anthropic/* │ │
|
||||
│ │ api.svc.plus/ui/* │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────▼──────────────────────┐
|
||||
│ LiteLLM Proxy (127.0.0.1:4000) │
|
||||
│ │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ /v1/chat/completions (OpenAI) │ │
|
||||
│ │ /v1/messages (Anthropic) │ │
|
||||
│ │ /ui (Admin Dashboard) │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└──────────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────────────────▼──────────────────────┐
|
||||
│ Model Providers (External) │
|
||||
│ │
|
||||
│ • OpenAI (GPT-4o-mini) │
|
||||
│ • Anthropic (Claude 3.5 Sonnet) │
|
||||
│ • DeepSeek (deepseek-chat) │
|
||||
│ • Local Models (OAI-compatible) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 推荐目录结构
|
||||
|
||||
```
|
||||
/etc/litellm/
|
||||
├── config.yaml # LiteLLM 配置文件
|
||||
└── litellm.env # 环境变量 (包含 API Keys)
|
||||
|
||||
/etc/systemd/system/
|
||||
└── litellm-proxy.service # systemd 服务单元
|
||||
|
||||
/etc/caddy/conf.d/
|
||||
└── litellm.caddy # Caddy 路由配置
|
||||
```
|
||||
|
||||
## 一、Caddyfile 配置示例
|
||||
|
||||
```caddy
|
||||
# /etc/caddy/conf.d/litellm.caddy
|
||||
|
||||
# API Gateway + LiteLLM Admin UI (统一入口)
|
||||
api.svc.plus {
|
||||
# LiteLLM Admin UI (Basic Auth 保护)
|
||||
@ui_admin {
|
||||
path /ui/*
|
||||
}
|
||||
|
||||
@ui_admin_unauthorized {
|
||||
not header Authorization "Basic *"
|
||||
}
|
||||
|
||||
handle @ui_admin_unauthorized {
|
||||
respond "Unauthorized" 401 {
|
||||
www-authenticate Basic realm="LiteLLM Admin UI"
|
||||
}
|
||||
}
|
||||
|
||||
handle @ui_admin {
|
||||
reverse_proxy 127.0.0.1:4000
|
||||
}
|
||||
|
||||
# OpenAI-Compatible API
|
||||
@openai_api {
|
||||
path /v1/openai/*
|
||||
}
|
||||
|
||||
handle @openai_api {
|
||||
rewrite * /v1{path}
|
||||
reverse_proxy 127.0.0.1:4000 {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
dial_timeout 30s
|
||||
read_timeout 600s
|
||||
write_timeout 600s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Anthropic-Compatible API
|
||||
@anthropic_api {
|
||||
path /v1/anthropic/*
|
||||
}
|
||||
|
||||
handle @anthropic_api {
|
||||
rewrite * /v1{path}
|
||||
reverse_proxy 127.0.0.1:4000 {
|
||||
flush_interval -1
|
||||
transport http {
|
||||
dial_timeout 30s
|
||||
read_timeout 600s
|
||||
write_timeout 600s
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# 通用代理
|
||||
handle {
|
||||
reverse_proxy 127.0.0.1:4000
|
||||
}
|
||||
|
||||
encode gzip zstd
|
||||
|
||||
header {
|
||||
X-Real-IP
|
||||
X-Forwarded-For
|
||||
X-Forwarded-Proto
|
||||
Host
|
||||
}
|
||||
|
||||
log {
|
||||
output file /var/log/caddy/litellm.access.log
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 关键路径映射
|
||||
|
||||
| 外部路径 | 内部路径 | 说明 |
|
||||
|---------------------------------------|--------------------------|--------------|
|
||||
| `https://api.svc.plus/v1/openai/chat/completions` | `http://127.0.0.1:4000/v1/chat/completions` | OpenAI 兼容 API |
|
||||
| `https://api.svc.plus/v1/anthropic/messages` | `http://127.0.0.1:4000/v1/messages` | Anthropic 兼容 API |
|
||||
| `https://api.svc.plus/ui/*` | `http://127.0.0.1:4000/ui/*` | Admin UI (Basic Auth) |
|
||||
| `https://api.svc.plus/v1/chat/completions` | `http://127.0.0.1:4000/v1/chat/completions` | 短路径兼容 (可选) |
|
||||
|
||||
---
|
||||
|
||||
## 二、LiteLLM config.yaml 示例
|
||||
|
||||
```yaml
|
||||
# /etc/litellm/config.yaml
|
||||
|
||||
model_list:
|
||||
# OpenAI 模型
|
||||
- model_name: gpt-4o-mini
|
||||
litellm_params:
|
||||
model: openai/gpt-4o-mini
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
|
||||
# Anthropic 模型
|
||||
- model_name: claude-sonnet
|
||||
litellm_params:
|
||||
model: anthropic/claude-3-5-sonnet-latest
|
||||
api_key: os.environ/ANTHROPIC_API_KEY
|
||||
|
||||
# DeepSeek 模型
|
||||
- model_name: deepseek-chat
|
||||
litellm_params:
|
||||
model: deepseek/deepseek-chat
|
||||
api_key: os.environ/DEEPSEEK_API_KEY
|
||||
|
||||
# 本地 OpenAI-Compatible 模型
|
||||
- model_name: local-qwen
|
||||
litellm_params:
|
||||
model: openai/qwen
|
||||
api_base: http://127.0.0.1:8000/v1
|
||||
api_key: os.environ/LOCAL_MODEL_API_KEY
|
||||
|
||||
general_settings:
|
||||
master_key: os.environ/LITELLM_MASTER_KEY
|
||||
drop_rate_limit_requests: true
|
||||
set_verbose: false
|
||||
|
||||
router_settings:
|
||||
model_group_alias:
|
||||
gpt-4o-mini: gpt-4o-mini
|
||||
claude-sonnet: claude-sonnet
|
||||
deepseek-chat: deepseek-chat
|
||||
routing_strategy: latency-based-routing
|
||||
enable_pre_call_checks: false
|
||||
retry_after: 60
|
||||
num_retries: 3
|
||||
|
||||
litellm_settings:
|
||||
drop_params: true
|
||||
set_verbose: true
|
||||
request_timeout: 600
|
||||
telemetry: false
|
||||
max_parallel_requests: 1000
|
||||
|
||||
environment_variables:
|
||||
OPENAI_API_KEY: os.environ/OPENAI_API_KEY
|
||||
ANTHROPIC_API_KEY: os.environ/ANTHROPIC_API_KEY
|
||||
DEEPSEEK_API_KEY: os.environ/DEEPSEEK_API_KEY
|
||||
LOCAL_MODEL_API_KEY: os.environ/LOCAL_MODEL_API_KEY
|
||||
LITELLM_MASTER_KEY: os.environ/LITELLM_MASTER_KEY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、litellm.env 示例
|
||||
|
||||
```bash
|
||||
# /etc/litellm/litellm.env
|
||||
|
||||
# API Keys (从环境变量读取)
|
||||
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
ANTHROPIC_API_KEY=sk-ant-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
LOCAL_MODEL_API_KEY=sk-local-placeholder
|
||||
|
||||
# LiteLLM Master Key (必须设置,用于 API 认证)
|
||||
LITELLM_MASTER_KEY=your-secure-random-master-key-here-min-32-chars
|
||||
|
||||
# 可选配置
|
||||
# LITELLM_SALT_KEY=your-salt-key
|
||||
# DATABASE_URL=postgresql://user:pass@host:5432/litellm
|
||||
```
|
||||
|
||||
**文件权限**: `chmod 600 /etc/litellm/litellm.env`
|
||||
|
||||
---
|
||||
|
||||
## 四、systemd 服务单元示例
|
||||
|
||||
```ini
|
||||
# /etc/systemd/system/litellm-proxy.service
|
||||
|
||||
[Unit]
|
||||
Description=LiteLLM Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
Group=ubuntu
|
||||
WorkingDirectory=/home/ubuntu
|
||||
EnvironmentFile=/etc/litellm/litellm.env
|
||||
ExecStart=/usr/local/bin/litellm \
|
||||
--host 127.0.0.1 \
|
||||
--port 4000 \
|
||||
--config /etc/litellm/config.yaml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=litellm-proxy
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、部署步骤
|
||||
|
||||
### 1. 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装 Python 和 pip
|
||||
apt update && apt install -y python3 python3-pip python3-venv
|
||||
|
||||
# 使用 pipx 安装 LiteLLM (推荐)
|
||||
pip install pipx
|
||||
pipx install litellm
|
||||
|
||||
# 或直接用 pip 安装
|
||||
pip install litellm
|
||||
```
|
||||
|
||||
### 2. 创建配置目录
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/litellm
|
||||
chmod 755 /etc/litellm
|
||||
```
|
||||
|
||||
### 3. 写入配置文件
|
||||
|
||||
```bash
|
||||
# 写入 config.yaml
|
||||
cat > /etc/litellm/config.yaml << 'EOF'
|
||||
model_list:
|
||||
- model_name: gpt-4o-mini
|
||||
litellm_params:
|
||||
model: openai/gpt-4o-mini
|
||||
api_key: os.environ/OPENAI_API_KEY
|
||||
# ... 其他模型
|
||||
EOF
|
||||
|
||||
# 写入环境变量文件
|
||||
cat > /etc/litellm/litellm.env << 'EOF'
|
||||
OPENAI_API_KEY=sk-xxx
|
||||
ANTHROPIC_API_KEY=sk-ant-xxx
|
||||
DEEPSEEK_API_KEY=sk-xxx
|
||||
LITELLM_MASTER_KEY=your-secure-master-key
|
||||
EOF
|
||||
|
||||
chmod 600 /etc/litellm/litellm.env
|
||||
chmod 640 /etc/litellm/config.yaml
|
||||
```
|
||||
|
||||
### 4. 部署 systemd 服务
|
||||
|
||||
```bash
|
||||
cat > /etc/systemd/system/litellm-proxy.service << 'EOF'
|
||||
[Unit]
|
||||
Description=LiteLLM Proxy Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=ubuntu
|
||||
Group=ubuntu
|
||||
WorkingDirectory=/home/ubuntu
|
||||
EnvironmentFile=/etc/litellm/litellm.env
|
||||
ExecStart=/usr/local/bin/litellm --host 127.0.0.1 --port 4000 --config /etc/litellm/config.yaml
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=litellm-proxy
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable litellm-proxy
|
||||
systemctl start litellm-proxy
|
||||
systemctl status litellm-proxy
|
||||
```
|
||||
|
||||
### 5. 配置 Caddy
|
||||
|
||||
```bash
|
||||
# 确保 Caddy 导入 conf.d 目录
|
||||
echo 'import /etc/caddy/conf.d/*.caddy' >> /etc/caddy/Caddyfile
|
||||
|
||||
# 创建 litellm Caddy 配置
|
||||
cat > /etc/caddy/conf.d/litellm.caddy << 'EOF'
|
||||
# ... 见上面的 Caddyfile 配置
|
||||
EOF
|
||||
|
||||
# 验证并重载
|
||||
caddy validate --config /etc/caddy/Caddyfile
|
||||
systemctl reload caddy
|
||||
```
|
||||
|
||||
### 6. 验证部署
|
||||
|
||||
```bash
|
||||
# 检查 LiteLLM 健康状态
|
||||
curl http://127.0.0.1:4000/health
|
||||
|
||||
# 检查 API Gateway
|
||||
curl -X POST "https://api.svc.plus/v1/openai/chat/completions" \
|
||||
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"model":"deepseek-chat","messages":[{"role":"user","content":"Hello"}]}'
|
||||
|
||||
# 访问 Admin UI
|
||||
# https://api.svc.plus/ui/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、API 验证命令
|
||||
|
||||
### 1. 健康检查
|
||||
|
||||
```bash
|
||||
# 本地健康检查
|
||||
curl http://127.0.0.1:4000/health
|
||||
|
||||
# 外部健康检查
|
||||
curl https://api.svc.plus/health
|
||||
```
|
||||
|
||||
### 2. OpenAI-Compatible API 测试
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.svc.plus/v1/openai/chat/completions" \
|
||||
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"model": "deepseek-chat",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello from OpenAI-compatible endpoint"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. Anthropic-Compatible API 测试
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.svc.plus/v1/anthropic/messages" \
|
||||
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "anthropic-version: 2023-06-01" \
|
||||
-d '{
|
||||
"model": "claude-sonnet",
|
||||
"max_tokens": 256,
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello from Anthropic-compatible endpoint"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. Admin UI 访问
|
||||
|
||||
```bash
|
||||
# 如果启用了 Basic Auth
|
||||
# 访问 https://api.svc.plus/ui/
|
||||
# 使用配置的 admin 用户名和密码登录
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、安全注意事项
|
||||
|
||||
### 1. 网络隔离
|
||||
|
||||
- **4000 端口只监听 127.0.0.1**,不暴露到公网
|
||||
- VPS 防火墙**不要开放 4000 端口**
|
||||
- 对外只开放 **443** (HTTPS)
|
||||
- **Caddy 是唯一公网入口**
|
||||
|
||||
### 2. Admin UI 保护
|
||||
|
||||
LiteLLM Admin UI **不应裸奔**,建议启用以下至少一种保护:
|
||||
|
||||
| 保护方式 | 说明 |
|
||||
|--------------|------------------------------|
|
||||
| Basic Auth | Caddy 内置,配置用户名密码 |
|
||||
| IP 白名单 | 只允许特定 IP 访问 api.svc.plus/ui |
|
||||
| Cloudflare Access | Cloudflare Zero Trust 认证 |
|
||||
| VPN / Tailscale | 通过私有网络访问 |
|
||||
|
||||
### 3. API 认证
|
||||
|
||||
- 所有 API 调用必须使用 `Authorization: Bearer <LITELLM_MASTER_KEY>`
|
||||
- `LITELLM_MASTER_KEY` 必须足够长且随机 (建议 32+ 字符)
|
||||
|
||||
### 4. 文件权限
|
||||
|
||||
```bash
|
||||
chmod 600 /etc/litellm/litellm.env # 保护 API Keys
|
||||
chmod 640 /etc/litellm/config.yaml # 配置文件
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、Ansible 部署命令
|
||||
|
||||
```bash
|
||||
# 部署 LiteLLM Gateway
|
||||
ansible-playbook -i inventory.ini setup-litellm.yaml
|
||||
|
||||
# 指定 API Keys 部署
|
||||
LITELLM_MASTER_KEY=your-secure-key \
|
||||
OPENAI_API_KEY=sk-xxx \
|
||||
ANTHROPIC_API_KEY=sk-ant-xxx \
|
||||
DEEPSEEK_API_KEY=sk-xxx \
|
||||
ansible-playbook -i inventory.ini setup-litellm.yaml
|
||||
|
||||
# 只部署 Caddy 配置 (不重启 LiteLLM)
|
||||
ansible-playbook -i inventory.ini setup-litellm.yaml --tags litellm --start-at-task="Create LiteLLM Caddy fragment"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、故障排查
|
||||
|
||||
### LiteLLM 服务无法启动
|
||||
|
||||
```bash
|
||||
# 查看日志
|
||||
journalctl -u litellm-proxy -f
|
||||
|
||||
# 验证配置
|
||||
litellm --config /etc/litellm/config.yaml --test
|
||||
```
|
||||
|
||||
### Caddy 配置无效
|
||||
|
||||
```bash
|
||||
# 验证 Caddy 配置
|
||||
caddy validate --config /etc/caddy/Caddyfile
|
||||
|
||||
# 查看 Caddy 日志
|
||||
tail -f /var/log/caddy/litellm-*.log
|
||||
```
|
||||
|
||||
### API 调用失败
|
||||
|
||||
```bash
|
||||
# 检查端口绑定
|
||||
ss -tlnp | grep 4000
|
||||
|
||||
# 测试本地连通性
|
||||
curl http://127.0.0.1:4000/health
|
||||
|
||||
# 检查 API Key
|
||||
source /etc/litellm/litellm.env
|
||||
echo $LITELLM_MASTER_KEY
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、后续扩展
|
||||
|
||||
### 启用 PostgreSQL 数据库 (用于用量统计、团队管理等)
|
||||
|
||||
```bash
|
||||
# 1. 安装 PostgreSQL
|
||||
apt install -y postgresql postgresql-contrib
|
||||
|
||||
# 2. 创建数据库和用户
|
||||
su - postgres
|
||||
psql -c "CREATE USER litellm WITH PASSWORD 'your-password';"
|
||||
psql -c "CREATE DATABASE litellm OWNER litellm;"
|
||||
exit
|
||||
|
||||
# 3. 更新环境变量
|
||||
echo "DATABASE_URL=postgresql://litellm:your-password@localhost:5432/litellm" >> /etc/litellm/litellm.env
|
||||
|
||||
# 4. 重启服务
|
||||
systemctl restart litellm-proxy
|
||||
```
|
||||
|
||||
### 集成 Vault (可选)
|
||||
|
||||
```bash
|
||||
# 设置 Vault 环境变量
|
||||
echo "VAULT_URL=https://vault.svc.plus" >> /etc/litellm/litellm.env
|
||||
echo "VAULT_API_KEY_PATH=secret/litellm/api-keys" >> /etc/litellm/litellm.env
|
||||
systemctl restart litellm-proxy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、Agent 接入配置
|
||||
|
||||
各 Agent 接入时只需配置 Base URL:
|
||||
|
||||
| Agent 类型 | Base URL | 认证 |
|
||||
|--------------|-----------------------------------|---------------|
|
||||
| OpenAI SDK | `https://api.svc.plus/v1/openai` | `LITELLM_MASTER_KEY` |
|
||||
| Anthropic SDK | `https://api.svc.plus/v1/anthropic` | `LITELLM_MASTER_KEY` |
|
||||
| LiteLLM SDK | `https://api.svc.plus` | `LITELLM_MASTER_KEY` |
|
||||
|
||||
示例 (Python):
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="your-litellm-master-key",
|
||||
base_url="https://api.svc.plus/v1/openai"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="deepseek-chat",
|
||||
messages=[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
```
|
||||
128
docs/setup-ai-workspace-all-in-one.md
Normal file
128
docs/setup-ai-workspace-all-in-one.md
Normal file
@ -0,0 +1,128 @@
|
||||
# AI Workspace 一键部署与全局安全网络配置向导
|
||||
|
||||
`setup-ai-workspace-all-in-one.yml` 是用于在目标 VPS 上完整、自动化地拉起 AI 研发环境底层组件与服务的聚合 Playbook。
|
||||
|
||||
> [!TIP]
|
||||
> ## ⏳ TL;DR (太长不看版)
|
||||
>
|
||||
> **一键标准部署 (无需配置任何前置环境,自带随机密钥保护):**
|
||||
> ```bash
|
||||
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | bash -
|
||||
> ```
|
||||
>
|
||||
> **一键极严防御部署 (瘫痪所有外网接口,强制全内网/VPN架构):**
|
||||
> ```bash
|
||||
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | AI_WORKSPACE_SECURITY_LEVEL=strict bash -
|
||||
> ```
|
||||
>
|
||||
> **组合技:极严防御 + 单独开白名单口子 (如仅开放 LiteLLM 接口):**
|
||||
> ```bash
|
||||
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | AI_WORKSPACE_SECURITY_LEVEL=strict LITELLM_API_CADDY_STRICT_WHITELIST=true bash -
|
||||
> ```
|
||||
>
|
||||
> **高级定制:一键部署全架构并按需开启可选功能 (如 XRDP,并自定义认证 Token):**
|
||||
> ```bash
|
||||
> curl -sfL https://raw.githubusercontent.com/ai-workspace-lab/xworkspace-console/main/scripts/setup-ai-workspace-all-in-one.sh | \
|
||||
> XWORKSPACE_CONSOLE_ENABLE_XRDP=true \
|
||||
> XWORKSPACE_CONSOLE_PUBLIC_ACCESS=true \
|
||||
> XWORKMATE_BRIDGE_PUBLIC_ACCESS=true \
|
||||
> GATEWAY_OPENCLAW_PUBLIC_ACCESS=false \
|
||||
> VAULT_PUBLIC_ACCESS=false \
|
||||
> LITELLM_API_CADDY_STRICT_WHITELIST=true \
|
||||
> DEPLOY_TOKEN="my-secure-custom-token-123" \
|
||||
> bash -
|
||||
> ```
|
||||
|
||||
本文档将详细介绍它的基础用法,并重点讲解如何通过内置的全局开关与细粒度 `public_access` 控制,打造出“最严安全网络架构”(断开一切外部 Web 端口代理,仅限加密 VPN 内网互联)。
|
||||
|
||||
## TODO
|
||||
|
||||
- [x] 等待并核对 `xworkspace-console` 的离线包 GitHub Actions 发布链路,确认 `publish-release` 完整结束且 release 产物上传成功。
|
||||
- [ ] 继续核对 `root@acp-bridge.onwalk.net` 的远程部署进度,确认 `setup-ai-workspace-all-in-one.sh` 最终完成并输出统一摘要。
|
||||
- [x] `setup-ai-workspace-all-in-one.sh` 在目标主机上优先使用离线安装包加速部署,减少在线拉取与安装耗时。
|
||||
- [ ] 验证 `setup-ai-workspace-all-in-one.sh` 幂等性:同一主机连续执行两次均成功,复用凭据、离线包缓存与已导入镜像,并安全等待部署/APT 锁。
|
||||
- [ ] 完成最终验收核对:Bridge 对外可达、其余服务默认仅本地监听、`acp-codex` / `opencode` / `gemini` / `hermes` / `qmd` / `litellm` 状态正常。
|
||||
- [ ] 记录最终提交哈希与远端验证结果,回填到本计划的交付结果部分。
|
||||
|
||||
---
|
||||
|
||||
## 1. 常规快速部署
|
||||
|
||||
如果您希望采用**标准(Standard)安全模式**部署(即:允许需要对外提供部分 Web/API 接口的应用如 `XWorkmate Bridge` 通过 HTTPS 暴露到公网,但内部组件互相隔离)。
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
|
||||
--limit jp-xhttp-contabo.svc.plus \
|
||||
--vault-password-file ~/.vault_password
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 极致安全:强制全隔离模式 (VPN Only)
|
||||
|
||||
如果您正在处理高敏感度的业务,或目标服务器被作为纯后台的 AI 基础设施节点。您可以选择将其配置为**最严的安全等级 (Strict)**。
|
||||
|
||||
在此模式下,任何默认开放外网的应用,都将被**强制剥夺公网入口(其 Caddy 代理配置或 K8s Ingress 将被直接销毁删除)。外部黑客或扫描器即便知道子域名,也无法解析请求到您的端口,此时访问服务器上的任何 AI 服务,全部必须经过内部加密隧道(例如 WireGuard / Tailscale 等 VPN 虚拟局域网)。**
|
||||
|
||||
**执行部署命令:**
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
|
||||
--limit jp-xhttp-contabo.svc.plus \
|
||||
--vault-password-file ~/.vault_password \
|
||||
-e "ai_workspace_security_level=strict"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 个性化服务放行与阻断 (-e 开关详解)
|
||||
|
||||
系统设计了精细化的权限参数,可以在 `standard` 安全模式的基础下,针对某个独立应用进行公网切断;又或者在 `strict` 极致安全模式的底座上,单独给某个应用“开一个白名单口子”。
|
||||
|
||||
### 全局策略控制开关
|
||||
- `-e "ai_workspace_security_level=strict"`
|
||||
* **作用:** 一键切断所有默认带有对外出口的组件。覆盖掉下述开关的默认策略,将其全部强转为 `false`。
|
||||
|
||||
### 细粒度服务暴露开关 (支持针对性覆盖)
|
||||
|
||||
1. **XWorkspace Console (底层主工作区门户) 公网访问控制**
|
||||
- **默认值:** `true` (standard 下) / `false` (strict 下)
|
||||
- **参数:** `-e "xworkspace_console_public_access=false"`
|
||||
- **作用:** 设为 true 时,会自动将本地 17000 端口通过 Caddy 反向代理到绑定的 `workspace.svc.plus` 域名提供公网访问。设为 false 时则销毁对应代理文件,只能进服务器内网/XRDP访问。
|
||||
|
||||
2. **XWorkmate Bridge 公网访问控制**
|
||||
- **默认值:** `true` (standard 下) / `false` (strict 下)
|
||||
- **参数:** `-e "xworkmate_bridge_public_access=false"`
|
||||
- **作用:** 设为 false 时,会彻底删除该服务在 Caddy `/etc/caddy/conf.d` 中的 `.caddy` 文件,使其失去从外界 HTTPS 进入内部 8787 端口的路径。
|
||||
|
||||
3. **OpenClaw Gateway 公网访问控制**
|
||||
- **默认值:** `false` (无论在何种策略下,底层模型网关默认不允许直接向公网打开界面入口)
|
||||
- **参数:** `-e "gateway_openclaw_public_access=true"`
|
||||
- **作用:** 当您在出差时,身边没有 VPN 环境,但迫切需要连接远程 OpenClaw 平台时,可以通过将其设为 true 临时生成 Caddy 文件,恢复它的公网域名入口访问。
|
||||
|
||||
4. **Vault KMS 密钥中心公网访问控制**
|
||||
- **默认值:** `false`
|
||||
- **参数:** `-e "vault_public_access=true"`
|
||||
- **作用:** 设为 false 时,该服务在 K8s 中部署的 Helm `ingress.enabled` 配置会被强制渲染为 false,不会向集群外网注册路由。设为 true 时方可绑定公网 Ingress Class 域名。
|
||||
|
||||
5. **LiteLLM 轻量网关访问行为控制**
|
||||
- **默认值:** `false`
|
||||
- **参数:** `-e "litellm_api_caddy_strict_whitelist=true"`
|
||||
- **作用:** 这个参数用于对 Caddy 代理行为做进一步保护,开启后,Caddy 会拦截一切没有命中官方兼容模型路径(如 `/v1/chat/completions`)的请求并拦截响应为 `404`,例如阻断前端 Dashboard UI(`/ui*`)的外网暴露。
|
||||
|
||||
6. **按需开启 XRDP 远程桌面连接**
|
||||
- **默认值:** `false`
|
||||
- **参数:** `-e "xworkspace_console_enable_xrdp=true"`
|
||||
- **作用:** XFCE 桌面环境默认仅提供基于 Web 浏览器的 Console UI,如需通过原生 RDP 客户端(如 Windows 远程桌面)连接目标主机,可增加此参数。
|
||||
|
||||
## 典型组合使用场景
|
||||
|
||||
**场景:开启 Strict 全局断网防护,但唯独开放 LiteLLM 模型 API 入口供第三方业务端点调用,且通过最严格白名单防护。**
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini setup-ai-workspace-all-in-one.yml \
|
||||
--limit jp-xhttp-contabo.svc.plus \
|
||||
--vault-password-file ~/.vault_password \
|
||||
-e "ai_workspace_security_level=strict" \
|
||||
-e "litellm_api_caddy_strict_whitelist=true"
|
||||
```
|
||||
这种精细的声明式管理,能确保基础设施按照 Infrastructure as Code (IaC) 的最佳安全实践被可预测地配置。
|
||||
108
docs/tldr-ssh-security.md
Normal file
108
docs/tldr-ssh-security.md
Normal file
@ -0,0 +1,108 @@
|
||||
# TLDR: SSH Security & Hardening Playbook
|
||||
|
||||
Quick reference for SSH security hardening, firewall controls, Fail2ban management, and connection checking.
|
||||
|
||||
## 1. SSH Hardening (Key-Only Auth)
|
||||
Password login is completely disabled for all users. Direct root login is restricted to key-only.
|
||||
|
||||
### Configuration file
|
||||
Drop-in config is deployed to:
|
||||
`/etc/ssh/sshd_config.d/00-disable-password.conf`
|
||||
|
||||
```text
|
||||
PasswordAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
KbdInteractiveAuthentication no
|
||||
PermitRootLogin prohibit-password
|
||||
```
|
||||
|
||||
### Apply Changes
|
||||
If you update SSH configurations, reload sshd:
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo systemctl reload ssh
|
||||
|
||||
# RedHat/CentOS
|
||||
sudo systemctl reload sshd
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Fail2ban Management
|
||||
Fail2ban monitors SSH authentication failures and bans offensive IPs.
|
||||
|
||||
### Default Settings
|
||||
* **Bantime**: 24 hours (`86400` seconds)
|
||||
* **Findtime**: 10 minutes (`600` seconds)
|
||||
* **Maxretry**: 3 attempts
|
||||
|
||||
### Useful Commands
|
||||
```bash
|
||||
# Check Fail2ban service status
|
||||
sudo systemctl status fail2ban
|
||||
|
||||
# Check sshd jail status (banned IPs)
|
||||
sudo fail2ban-client status sshd
|
||||
|
||||
# Unban a specific IP
|
||||
sudo fail2ban-client set sshd unbanip <IP>
|
||||
|
||||
# Manually ban a specific IP
|
||||
sudo fail2ban-client set sshd banip <IP>
|
||||
|
||||
# View fail2ban logs
|
||||
sudo tail -f /var/log/fail2ban.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. SSH Proxy Connection Helper (`ssh_check.exp`)
|
||||
A generic `expect` helper script to verify ProxyJump-ed SSH connectivity.
|
||||
|
||||
### Usage
|
||||
To prevent password leaks in shell history (`~/.bash_history` or `~/.zsh_history`), **never** pass the password as a command-line argument. Instead, use one of the secure methods below:
|
||||
|
||||
#### Option A: Read securely from input (Recommended)
|
||||
```bash
|
||||
# Type your password securely (input will not echo on screen)
|
||||
read -s SSH_CHECK_PASSWORD
|
||||
export SSH_CHECK_PASSWORD
|
||||
|
||||
# Run the helper script (picks up password from env var)
|
||||
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129
|
||||
```
|
||||
|
||||
#### Option B: Set via env var with leading space
|
||||
If your shell is configured to ignore commands starting with a space (e.g. `HISTCONTROL=ignorespace` in bash or `setopt HIST_IGNORE_SPACE` in zsh), you can set the variable with a leading space:
|
||||
```bash
|
||||
export SSH_CHECK_PASSWORD="your_password"
|
||||
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129
|
||||
```
|
||||
|
||||
#### Option C: Legacy/Direct (Not recommended, leaves history trace)
|
||||
```bash
|
||||
ssh_check.exp admin@tky-proxy.svc.plus root@167.179.110.129 "your_password"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Firewall (UFW) quick-ref
|
||||
Used on hosts to manage ports (e.g. 80, 443, 1443).
|
||||
|
||||
```bash
|
||||
# View firewall rules with line numbers
|
||||
sudo ufw status numbered
|
||||
|
||||
# Allow a port to Anywhere
|
||||
sudo ufw allow 443/tcp
|
||||
|
||||
# Delete a rule by rule number
|
||||
sudo ufw delete <rule_number>
|
||||
|
||||
# Restrict port 22 to a specific IP (e.g. Proxy IP)
|
||||
sudo ufw allow from 43.207.194.92 to any port 22 proto tcp
|
||||
sudo ufw delete allow 22/tcp
|
||||
|
||||
# Reload firewall
|
||||
sudo ufw reload
|
||||
```
|
||||
205
docs/yitu-it-series-r2-assets.md
Normal file
205
docs/yitu-it-series-r2-assets.md
Normal file
@ -0,0 +1,205 @@
|
||||
# yitu-it-series R2 assets
|
||||
|
||||
This runbook migrates the local Google Drive `自媒体` directory to Cloudflare R2 for the Docusaurus AI Native knowledge base.
|
||||
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
GitHub -> Docusaurus -> Cloudflare Pages -> ebook.svc.plus
|
||||
|
||||
Google Drive local folder
|
||||
-> rclone
|
||||
-> Cloudflare R2 bucket: yitu-it-series
|
||||
-> R2 custom domain: img.svc.plus
|
||||
-> Docusaurus Markdown image URLs
|
||||
```
|
||||
|
||||
## Source and target
|
||||
|
||||
```text
|
||||
Local source:
|
||||
/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
|
||||
|
||||
R2 bucket:
|
||||
yitu-it-series
|
||||
|
||||
Public asset domain:
|
||||
https://img.svc.plus
|
||||
```
|
||||
|
||||
## Recommended object layout
|
||||
|
||||
```text
|
||||
yitu-it-series/
|
||||
├── covers/
|
||||
├── xiaohongshu/
|
||||
├── observability/
|
||||
├── storage/
|
||||
├── networking/
|
||||
├── ai-native/
|
||||
├── security/
|
||||
├── platform-engineering/
|
||||
└── ebook-assets/
|
||||
```
|
||||
|
||||
Use stable, semantic paths for published content:
|
||||
|
||||
```text
|
||||
covers/season-1/single-machine-to-platform-cover-v1.png
|
||||
security/least-privilege/root-to-rootless-v1.png
|
||||
ai-native/agentic-infra/ai-native-platform-v1.png
|
||||
ebook-assets/diagrams/cloud-native-to-ai-native-v1.png
|
||||
```
|
||||
|
||||
Prefer versioned object names instead of overwriting an already published image. This keeps Cloudflare CDN behavior predictable and preserves old articles.
|
||||
|
||||
## Cloudflare API token
|
||||
|
||||
Create two token scopes if possible:
|
||||
|
||||
```text
|
||||
Bootstrap token:
|
||||
- Account: Cloudflare R2: Edit
|
||||
- Zone: DNS: Edit, Zone: Read for svc.plus
|
||||
- Used only for bucket/custom-domain setup
|
||||
|
||||
Long-running R2 S3 token:
|
||||
- R2 Object Read & Write
|
||||
- Scope limited to bucket yitu-it-series
|
||||
- Used by rclone sync
|
||||
```
|
||||
|
||||
Required environment variables:
|
||||
|
||||
```bash
|
||||
export CF_ACCOUNT_ID="..."
|
||||
export CF_ZONE_ID="..."
|
||||
export CLOUDFLARE_API_TOKEN="..."
|
||||
export R2_ACCESS_KEY_ID="..."
|
||||
export R2_SECRET_ACCESS_KEY="..."
|
||||
```
|
||||
|
||||
## Commands
|
||||
|
||||
From the playbooks directory:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
chmod +x scripts/sync-yitu-it-series-r2.sh
|
||||
|
||||
scripts/sync-yitu-it-series-r2.sh doctor
|
||||
scripts/sync-yitu-it-series-r2.sh create-bucket
|
||||
scripts/sync-yitu-it-series-r2.sh configure-rclone
|
||||
scripts/sync-yitu-it-series-r2.sh dry-run
|
||||
scripts/sync-yitu-it-series-r2.sh copy
|
||||
scripts/sync-yitu-it-series-r2.sh check
|
||||
scripts/sync-yitu-it-series-r2.sh tree
|
||||
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
|
||||
```
|
||||
|
||||
Use `copy` for the first production migration when preserving all historical remote files matters. Use `sync` for steady-state mirroring after the source layout is stable.
|
||||
|
||||
## Performance profile
|
||||
|
||||
Default large AI image profile:
|
||||
|
||||
```bash
|
||||
export RCLONE_TRANSFERS=16
|
||||
export RCLONE_CHECKERS=32
|
||||
export RCLONE_S3_UPLOAD_CUTOFF=128M
|
||||
export RCLONE_S3_CHUNK_SIZE=128M
|
||||
```
|
||||
|
||||
Many small images:
|
||||
|
||||
```bash
|
||||
export RCLONE_TRANSFERS=32
|
||||
export RCLONE_CHECKERS=64
|
||||
```
|
||||
|
||||
Large source files such as PSD/video:
|
||||
|
||||
```bash
|
||||
export RCLONE_TRANSFERS=4
|
||||
export RCLONE_CHECKERS=16
|
||||
export RCLONE_S3_UPLOAD_CUTOFF=256M
|
||||
export RCLONE_S3_CHUNK_SIZE=256M
|
||||
```
|
||||
|
||||
## Incremental sync
|
||||
|
||||
Install a macOS launchd sync job:
|
||||
|
||||
```bash
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks
|
||||
scripts/sync-yitu-it-series-r2.sh install-launchd
|
||||
launchctl list | grep yitu-it-series
|
||||
```
|
||||
|
||||
Remove it:
|
||||
|
||||
```bash
|
||||
scripts/sync-yitu-it-series-r2.sh uninstall-launchd
|
||||
```
|
||||
|
||||
## R2 custom domain
|
||||
|
||||
Target:
|
||||
|
||||
```text
|
||||
img.svc.plus -> R2 bucket yitu-it-series
|
||||
```
|
||||
|
||||
The script calls the Cloudflare R2 custom domain API:
|
||||
|
||||
```bash
|
||||
scripts/sync-yitu-it-series-r2.sh configure-custom-domain
|
||||
```
|
||||
|
||||
Recommended Cloudflare cache rule:
|
||||
|
||||
```text
|
||||
If hostname equals img.svc.plus:
|
||||
- Cache eligible
|
||||
- Edge TTL: 30 days or longer
|
||||
- Browser TTL: 7-30 days, or respect origin
|
||||
```
|
||||
|
||||
## Docusaurus references
|
||||
|
||||
Markdown:
|
||||
|
||||
```md
|
||||

|
||||

|
||||
```
|
||||
|
||||
MDX:
|
||||
|
||||
```mdx
|
||||
<img
|
||||
src="https://img.svc.plus/platform-engineering/platform-engineering-roadmap-v1.png"
|
||||
alt="Platform Engineering Roadmap"
|
||||
loading="lazy"
|
||||
/>
|
||||
```
|
||||
|
||||
Front matter:
|
||||
|
||||
```md
|
||||
---
|
||||
title: AI Native 基础设施演进
|
||||
description: 从云原生到 AI Native 的平台工程知识库
|
||||
image: https://img.svc.plus/covers/ai-native-infra-cover-v1.png
|
||||
---
|
||||
```
|
||||
|
||||
## AI Native knowledge-base practices
|
||||
|
||||
- Keep Docusaurus focused on Markdown, MDX, navigation, SEO, and search.
|
||||
- Keep heavy generated images and ebook assets in R2.
|
||||
- Reference published assets with absolute `https://img.svc.plus/...` URLs.
|
||||
- Keep object names immutable after publication; publish revisions with `-v2`, `-v3`.
|
||||
- Run `rclone check` before replacing local Markdown image references.
|
||||
- Keep raw generation artifacts separate from article-ready assets when possible.
|
||||
- Use topic directories that match the ebook taxonomy so future RAG/vector indexing can attach image context to chapters.
|
||||
7
examples/deploy-xworkspace-portal.md
Normal file
7
examples/deploy-xworkspace-portal.md
Normal file
@ -0,0 +1,7 @@
|
||||
cd /Users/shenlan/workspaces/cloud-neutral-toolkit/playbooks && ansible-playbook \
|
||||
-i "xworkmate-bridge.svc.plus," \
|
||||
--user ubuntu \
|
||||
-e "xworkspace_console_hosts=xworkmate-bridge.svc.plus" \
|
||||
-e "xworkspace_console_local_dashboard_dir=/home/ubuntu/xworkspace/dashboard" \
|
||||
-e "ansible_become_pass=XXXXXXXXX" \
|
||||
setup-xworkspace-console.yaml
|
||||
15
examples/yitu-it-series-r2.env.example
Normal file
15
examples/yitu-it-series-r2.env.example
Normal file
@ -0,0 +1,15 @@
|
||||
CF_ACCOUNT_ID=
|
||||
CF_ZONE_ID=
|
||||
CLOUDFLARE_API_TOKEN=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
|
||||
R2_BUCKET=yitu-it-series
|
||||
R2_REMOTE=cloudflare-r2
|
||||
R2_CUSTOM_DOMAIN=img.svc.plus
|
||||
LOCAL_SRC=/Users/shenlan/Library/CloudStorage/GoogleDrive-haitaopanhq@gmail.com/我的云端硬盘/自媒体
|
||||
|
||||
RCLONE_TRANSFERS=16
|
||||
RCLONE_CHECKERS=32
|
||||
RCLONE_S3_UPLOAD_CUTOFF=128M
|
||||
RCLONE_S3_CHUNK_SIZE=128M
|
||||
7
gnome_xrdp_minimal.yaml
Normal file
7
gnome_xrdp_minimal.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Setup minimal GNOME + XRDP desktop
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- roles/vhosts/gnome_xrdp_minimal/
|
||||
8
gpu_inference_01_prepare.yml
Normal file
8
gpu_inference_01_prepare.yml
Normal file
@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Prepare Host Environment
|
||||
hosts: all
|
||||
become: true
|
||||
roles:
|
||||
- roles/vhosts/common
|
||||
- roles/vhosts/kernel_tuning
|
||||
- roles/docker/container_runtime
|
||||
7
gpu_inference_02_sealos.yml
Normal file
7
gpu_inference_02_sealos.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Install Kubernetes via Sealos
|
||||
hosts: masters
|
||||
become: true
|
||||
roles:
|
||||
- roles/vhosts/sealos_cluster
|
||||
- roles/vhosts/cni_cilium
|
||||
6
gpu_inference_03_gpu_operator.yml
Normal file
6
gpu_inference_03_gpu_operator.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Install NVIDIA GPU Operator
|
||||
hosts: masters[0]
|
||||
become: true
|
||||
roles:
|
||||
- roles/charts/nvidia_gpu_operator
|
||||
7
gpu_inference_04_ray.yml
Normal file
7
gpu_inference_04_ray.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Deploy Ray Cluster
|
||||
hosts: masters[0]
|
||||
become: true
|
||||
roles:
|
||||
- roles/charts/ray_cluster
|
||||
- roles/charts/ray_service
|
||||
7
gpu_inference_05_vllm.yml
Normal file
7
gpu_inference_05_vllm.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Deploy vLLM Inference Service
|
||||
hosts: masters[0]
|
||||
become: true
|
||||
roles:
|
||||
- roles/charts/vllm_runtime
|
||||
- roles/charts/vllm_service
|
||||
6
gpu_inference_site.yml
Normal file
6
gpu_inference_site.yml
Normal file
@ -0,0 +1,6 @@
|
||||
---
|
||||
- import_playbook: gpu_inference_01_prepare.yml
|
||||
- import_playbook: gpu_inference_02_sealos.yml
|
||||
- import_playbook: gpu_inference_03_gpu_operator.yml
|
||||
- import_playbook: gpu_inference_04_ray.yml
|
||||
- import_playbook: gpu_inference_05_vllm.yml
|
||||
@ -3,3 +3,17 @@ ansible_ssh_user: root
|
||||
ansible_ssh_private_key_file: ~/.ssh/id_rsa
|
||||
ansible_host_key_checking: False
|
||||
|
||||
# Global security level for public access.
|
||||
# Set to 'strict' to disable public Caddy/Ingress access for all roles.
|
||||
ai_workspace_security_level: standard
|
||||
|
||||
# Caddy ingress is enabled by default on Linux where we expect a dedicated box.
|
||||
# It is disabled on macOS (developer workstation with port conflicts) and Windows
|
||||
# (Caddy not natively supported in our Windows pipeline).
|
||||
# Override anytime with -e caddy_enabled=true or -e caddy_enabled=false.
|
||||
caddy_enabled: "{{ ansible_os_family != 'Darwin' and ansible_os_family != 'Windows' }}"
|
||||
|
||||
# Caddy config root. Linux uses the system path /etc/caddy; macOS (Homebrew)
|
||||
# uses /opt/homebrew/etc/caddy. Roles derive their Caddyfile / conf.d / fragment
|
||||
# paths from this so a force-enabled Caddy on macOS writes to the brew location.
|
||||
caddy_config_dir: "{{ '/opt/homebrew/etc/caddy' if ansible_os_family == 'Darwin' else '/etc/caddy' }}"
|
||||
|
||||
49
group_vars/xworkmate_bridge_distributed.yml
Normal file
49
group_vars/xworkmate_bridge_distributed.yml
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
xworkmate_bridge_distributed_topology: dual-node
|
||||
xworkmate_bridge_distributed_nodes:
|
||||
- id: xworkmate-bridge
|
||||
role: primary
|
||||
public_base_url: https://xworkmate-bridge.svc.plus
|
||||
bridge_endpoint: http://172.29.10.1:8787
|
||||
- id: cn-xworkmate-bridge
|
||||
role: edge
|
||||
public_base_url: https://cn-xworkmate-bridge.svc.plus
|
||||
bridge_endpoint: http://172.29.10.2:8787
|
||||
|
||||
xworkmate_bridge_distributed_vpn_interface: wg-xwm
|
||||
xworkmate_bridge_distributed_vpn_wireguard_port: 51820
|
||||
xworkmate_bridge_distributed_vpn_local_tproxy_port: 51830
|
||||
xworkmate_bridge_distributed_vpn_vless_port: 2443
|
||||
xworkmate_bridge_distributed_vpn_forwarder_port: 8787
|
||||
xworkmate_bridge_distributed_vpn_forwarder_target: 127.0.0.1:8787
|
||||
xworkmate_bridge_distributed_vpn_vault_addr: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_URL') | default('https://vault.svc.plus', true) }}"
|
||||
xworkmate_bridge_distributed_vpn_vault_token: "{{ lookup('ansible.builtin.env', 'VAULT_SERVER_ROOT_ACCESS_TOKEN') | default(lookup('ansible.builtin.env', 'VAULT_TOKEN'), true) }}"
|
||||
xworkmate_bridge_distributed_vpn_vault_mount: kv
|
||||
xworkmate_bridge_distributed_vpn_vault_base_path: xworkmate-bridge/distributed/wireguard-over-vless
|
||||
|
||||
xworkmate_bridge_distributed_vpn_nodes:
|
||||
jp-xhttp-contabo.svc.plus:
|
||||
node_id: xworkmate-bridge
|
||||
domain: xworkmate-bridge.svc.plus
|
||||
wg_ip: 172.29.10.1
|
||||
public_key: 1staGq8lmHFRFRFNj2QOFx/MPxb/1fFV4tawC6xSi1Q= # gitleaks:allow
|
||||
peer: cn-xworkmate-bridge.svc.plus
|
||||
cn-xworkmate-bridge.svc.plus:
|
||||
node_id: cn-xworkmate-bridge
|
||||
domain: cn-xworkmate-bridge.svc.plus
|
||||
wg_ip: 172.29.10.2
|
||||
public_key: iYlnFaWiMfMelpiN8ZV2SwCDrLihqtJXvHUsM3BN9zU= # gitleaks:allow
|
||||
peer: jp-xhttp-contabo.svc.plus
|
||||
|
||||
xworkmate_bridge_distributed_vpn_clients:
|
||||
- id: shenlan-macos
|
||||
wg_ip: 172.29.10.10
|
||||
public_key: jfHsw1HIqRQzGvfsRfdkS7BLThDbBvWMsAlJRp1kdkw= # gitleaks:allow
|
||||
attach_to:
|
||||
- jp-xhttp-contabo.svc.plus
|
||||
- cn-xworkmate-bridge.svc.plus
|
||||
- id: shenlan-ios
|
||||
wg_ip: 172.29.10.11
|
||||
public_key: I/zCL7gLWrY6FZiLXUs7i/vivU5Xuo8r7EbkNhtv12w= # gitleaks:allow
|
||||
attach_to:
|
||||
- jp-xhttp-contabo.svc.plus
|
||||
17
harden_ssh_root_key_only.yml
Normal file
17
harden_ssh_root_key_only.yml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
- name: Harden SSH on all inventory hosts
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
|
||||
vars:
|
||||
sshd_config_path: /etc/ssh/sshd_config
|
||||
sshd_dropin_dir: /etc/ssh/sshd_config.d
|
||||
root_authorized_keys_path: /root/.ssh/authorized_keys
|
||||
local_public_key_path: "{{ lookup('env', 'HOME') }}/.ssh/id_rsa.pub"
|
||||
ansible_user: "{{ lookup('env', 'BOOTSTRAP_ROOT_USER') | default('root', true) }}"
|
||||
ansible_password: "{{ lookup('env', 'BOOTSTRAP_ROOT_PASSWORD') | default(omit, true) }}"
|
||||
ansible_become_password: "{{ lookup('env', 'BOOTSTRAP_BECOME_PASSWORD') | default(omit, true) }}"
|
||||
|
||||
roles:
|
||||
- role: harden_ssh_root_key_only
|
||||
16
host_vars/cn-xworkmate-bridge.svc.plus.yml
Normal file
16
host_vars/cn-xworkmate-bridge.svc.plus.yml
Normal file
@ -0,0 +1,16 @@
|
||||
---
|
||||
xworkmate_bridge_domain: cn-xworkmate-bridge.svc.plus
|
||||
xworkmate_bridge_public_base_url: https://cn-xworkmate-bridge.svc.plus
|
||||
xworkmate_bridge_service_domain: cn-xworkmate-bridge.svc.plus
|
||||
xworkmate_bridge_service_public_base_url: https://cn-xworkmate-bridge.svc.plus
|
||||
xworkmate_bridge_binary_path: /usr/local/bin/xworkmate-bridge
|
||||
xworkmate_bridge_service_user: ubuntu
|
||||
xworkmate_bridge_service_group: ubuntu
|
||||
xworkmate_bridge_service_home: /home/ubuntu
|
||||
xworkmate_bridge_required_services: []
|
||||
xworkmate_bridge_required_listeners:
|
||||
- host: 127.0.0.1
|
||||
port: "8787"
|
||||
name: bridge
|
||||
xworkmate_bridge_distributed_local_node_id: cn-xworkmate-bridge
|
||||
xworkmate_bridge_distributed_task_forward_peer_id: xworkmate-bridge
|
||||
2
host_vars/jp-xhttp-contabo.svc.plus/gateway_openclaw.yml
Normal file
2
host_vars/jp-xhttp-contabo.svc.plus/gateway_openclaw.yml
Normal file
@ -0,0 +1,2 @@
|
||||
---
|
||||
gateway_openclaw_acp_enabled: true
|
||||
25
host_vars/jp-xhttp-contabo.svc.plus/litellm.yml
Normal file
25
host_vars/jp-xhttp-contabo.svc.plus/litellm.yml
Normal file
@ -0,0 +1,25 @@
|
||||
---
|
||||
# LiteLLM Admin UI Credentials
|
||||
litellm_basic_auth_username: admin
|
||||
|
||||
# Database Configuration
|
||||
litellm_database_host: "127.0.0.1"
|
||||
litellm_database_port: "15432"
|
||||
litellm_database_sslmode: "disable"
|
||||
litellm_database_name: "litellm"
|
||||
litellm_database_user: "litellm"
|
||||
litellm_database_password: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
33303762616661623962386664653533666362393435343830303061613364666238313933626330
|
||||
6231303463663732313732376238633033386463383134630a313738393762333263653363376266
|
||||
37323938386331383762363565613361623638353834363735363030363037626666663431613239
|
||||
3939323036313435360a313833316131663231326162393364616262323763333133336335323837
|
||||
31336464646334653035646633363164633363353835316466626337633238396130
|
||||
|
||||
litellm_master_key: !vault |
|
||||
$ANSIBLE_VAULT;1.1;AES256
|
||||
38303433656665323039303561326534636136623766303563313863633133333564343830663032
|
||||
3038343634323835666430663165343461643338343334330a623764393034356330303263366161
|
||||
30663735663237653135356663343063353330356137643534313637313062633964383266376263
|
||||
6432333730393232380a363037333265363462323139306534343563323631616464616132313631
|
||||
31396232386633656436653966626131663139643539633964633864643930643639
|
||||
@ -0,0 +1,3 @@
|
||||
---
|
||||
xworkmate_bridge_distributed_local_node_id: xworkmate-bridge
|
||||
xworkmate_bridge_distributed_task_forward_peer_id: ""
|
||||
@ -1,32 +1,88 @@
|
||||
# Vhosts
|
||||
[cn_front_host]
|
||||
# services: cn-front.svc.plus, cn-homepage.svc.plus
|
||||
cn-front.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root firewall_manage_ufw=false service_domains=cn-front.svc.plus
|
||||
|
||||
[cn_homepage_host]
|
||||
# services: cn-homepage.svc.plus
|
||||
cn-homepage.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root
|
||||
|
||||
[cn_xworkmate_bridge_host]
|
||||
# services: cn-xworkmate-bridge.svc.plus
|
||||
cn-xworkmate-bridge.svc.plus ansible_host=47.120.61.35 ansible_user=root ansible_ssh_user=root service_domains=cn-xworkmate-bridge.svc.plus
|
||||
|
||||
[global_homepage_host]
|
||||
# services: global-homepage.svc.plus
|
||||
global-homepage.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root
|
||||
|
||||
[jp_xhttp_contabo_host]
|
||||
# services: api.svc.plus, console.svc.plus, docs.svc.plus, accounts.svc.plus, xworkmate-bridge.svc.plus, xworkmate-bridge.svc.plus, vault.svc.plus, postgresql.svc.plus
|
||||
jp-xhttp-contabo.svc.plus ansible_host=46.250.251.132 ansible_user=root ansible_ssh_user=root service_domains=api.svc.plus,console.svc.plus,docs.svc.plus,accounts.svc.plus,xworkmate-bridge.svc.plus,xworkmate-bridge.svc.plus,vault.svc.plus,postgresql.svc.plus xray_exporter_node_id_custom=jp-xhttp-contabo.svc.plus
|
||||
|
||||
[tky_proxy_host]
|
||||
# services: tky-proxy.svc.plus
|
||||
tky-proxy.svc.plus ansible_host=43.207.194.92 ansible_user=admin ansible_ssh_user=admin service_domains=tky-proxy.svc.plus
|
||||
|
||||
[us_xhttp_host]
|
||||
# services: tky-proxy.svc.plus
|
||||
us-xhttp.svc.plus ansible_host=44.251.164.174 ansible_user=ubuntu ansible_ssh_user=admin service_domains=us-xhttp.svc.plus
|
||||
|
||||
[jp_k3s_vultr_host]
|
||||
# services: jp-k3s-vultr.svc.plus
|
||||
jp-k3s-vultr.svc.plus ansible_host=167.179.110.129 ansible_user=root ansible_ssh_user=root service_domains=jp-k3s-vultr.svc.plus
|
||||
|
||||
# Logical service groups
|
||||
[web]
|
||||
cn-console.svc.plus ansible_host=47.120.61.35
|
||||
global-console.svc.plus ansible_host=35.220.157.80 ansible_user=root
|
||||
cn-front.svc.plus
|
||||
|
||||
[deepflow_agents]
|
||||
192.168.1.101 ansible_user=root ansible_ssh_pass=pass101
|
||||
192.168.1.102 ansible_user=admin ansible_ssh_pass=pass102
|
||||
192.168.1.103 ansible_user=root ansible_ssh_pass=pass103 ansible_port=2222
|
||||
192.168.1.104 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa_ubuntu
|
||||
[agent_svc_plus]
|
||||
tky-proxy.svc.plus
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[mail]
|
||||
smtp.svc.plus ansible_host=45.130.167.90
|
||||
[xray_exporter]
|
||||
tky-proxy.svc.plus
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[bootstrap]
|
||||
auth.svc.plus ansible_host=34.92.122.119 ansible_user=root ansible_ssh_private_key_file=~/.ssh/id_rsa
|
||||
[xworkmate_bridge]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[cn_xworkmate_bridge]
|
||||
cn-xworkmate-bridge.svc.plus
|
||||
|
||||
[xworkmate_bridge_distributed:children]
|
||||
xworkmate_bridge
|
||||
cn_xworkmate_bridge
|
||||
|
||||
[billing_service]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[accounts]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[docs]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[apisix]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[apisix]
|
||||
api.svc.plus ansible_host=46.250.251.132 ansible_user=root
|
||||
|
||||
[postgresql]
|
||||
jp-xhttp-contabo.svc.plus
|
||||
|
||||
[k3s]
|
||||
jp-k3s-vultr.svc.plus
|
||||
|
||||
[all:vars]
|
||||
ansible_port=22
|
||||
ansible_user=root
|
||||
ansible_ssh_transfer_method=piped
|
||||
ansible_host_key_checking=False
|
||||
|
||||
# SSH 密钥或密码(二选一)
|
||||
# ansible_ssh_private_key_file=~/.ssh/id_rsa
|
||||
# ansible_ssh_pass=your_password
|
||||
ansible_ssh_private_key_file=~/.ssh/id_rsa
|
||||
k3s_platform_git_private_key=~/.ssh/id_rsa
|
||||
|
||||
# DeepFlow agent 配置变量
|
||||
controller_ips=["10.10.10.10", "10.10.10.11"]
|
||||
vtap_group_id="g-P22vLIMdB6"
|
||||
|
||||
# DeepFlow agent 安装包位置
|
||||
agent_base_dir="deepflow-agent-for-linux"
|
||||
agent_package_name="deepflow-agent-1.0-5407.systemd.x86_64.rpm"
|
||||
[acp_bridge_host]
|
||||
acp-bridge.onwalk.net ansible_host=167.179.110.129 ansible_user=root ansible_ssh_user=root
|
||||
|
||||
27
inventory/group_vars/all.yml
Normal file
27
inventory/group_vars/all.yml
Normal file
@ -0,0 +1,27 @@
|
||||
---
|
||||
# 全局版本与镜像
|
||||
kubernetes_version: "v1.28.9"
|
||||
sealos_version: "5.0.0"
|
||||
cilium_version: "1.15.5"
|
||||
gpu_operator_version: "v24.3.0"
|
||||
kuberay_version: "1.1.0"
|
||||
ray_version: "2.9.0"
|
||||
vllm_image: "vllm/vllm-openai:v0.4.2"
|
||||
|
||||
# 网络配置
|
||||
pod_cidr: "10.244.0.0/16"
|
||||
service_cidr: "10.96.0.0/12"
|
||||
nccl_socket_ifname: "eth0"
|
||||
gloo_socket_ifname: "eth0"
|
||||
|
||||
# 模型与推理配置
|
||||
vllm_model: "/models/Llama-3-70B-Instruct"
|
||||
vllm_tensor_parallel_size: 2
|
||||
vllm_pipeline_parallel_size: 1
|
||||
|
||||
# GPU 驱动策略
|
||||
driver_enabled: true
|
||||
driver_version: "535.129.03"
|
||||
dcgm_exporter_enabled: true
|
||||
|
||||
ansible_user: "root"
|
||||
1
inventory/group_vars/gpu_workers.yml
Normal file
1
inventory/group_vars/gpu_workers.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
1
inventory/group_vars/masters.yml
Normal file
1
inventory/group_vars/masters.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
1
inventory/group_vars/ray_workers.yml
Normal file
1
inventory/group_vars/ray_workers.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
13
inventory/hosts.ini
Normal file
13
inventory/hosts.ini
Normal file
@ -0,0 +1,13 @@
|
||||
[masters]
|
||||
k8s-master-01 ansible_host=10.0.0.10
|
||||
|
||||
[gpu_workers]
|
||||
k8s-gpu-01 ansible_host=10.0.0.21 accelerator=nvidia-h100
|
||||
k8s-gpu-02 ansible_host=10.0.0.22 accelerator=nvidia-h100
|
||||
|
||||
[ray_workers:children]
|
||||
gpu_workers
|
||||
|
||||
[k8s_cluster:children]
|
||||
masters
|
||||
gpu_workers
|
||||
106
inventory/terraform_cmdb.py
Executable file
106
inventory/terraform_cmdb.py
Executable file
@ -0,0 +1,106 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Ansible 动态 inventory —— 数据源为 Terraform 导出的 CMDB。
|
||||
|
||||
与 IAC 联动方式:
|
||||
iac_modules/terraform-hcl-standard/vultr-vps/envs/ai-workspace/ 的 generate.py
|
||||
在 `terraform apply` 后,把 YAML 静态字段与 terraform 运行时输出合并写出
|
||||
cmdb.json(结构化主机事实)。本脚本把它翻译成 Ansible 动态 inventory,
|
||||
于是 IaC 一变更、重跑 `generate.py inventory`,inventory 就跟着变。
|
||||
|
||||
取数优先级:
|
||||
1. 环境变量 AI_WORKSPACE_CMDB_JSON 指向的文件
|
||||
2. 环境变量 AI_WORKSPACE_TF_DIR(或默认 env 目录)下的 cmdb.json
|
||||
|
||||
用法:
|
||||
ansible-inventory -i inventory/terraform_cmdb.py --list
|
||||
ansible all -i inventory/terraform_cmdb.py -m ping
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
# playbooks/inventory -> 仓库根 -> terraform env
|
||||
REPO_ROOT = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
DEFAULT_TF_DIR = os.path.join(
|
||||
REPO_ROOT,
|
||||
"iac_modules",
|
||||
"terraform-hcl-standard",
|
||||
"vultr-vps",
|
||||
"envs",
|
||||
"ai-workspace",
|
||||
)
|
||||
|
||||
|
||||
def _from_explicit_file():
|
||||
path = os.environ.get("AI_WORKSPACE_CMDB_JSON")
|
||||
if path and os.path.isfile(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
return None
|
||||
|
||||
|
||||
def _from_default_file(tf_dir):
|
||||
path = os.path.join(tf_dir, "cmdb.json")
|
||||
if os.path.isfile(path):
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
return json.load(fh)
|
||||
return None
|
||||
|
||||
|
||||
def load_cmdb():
|
||||
tf_dir = os.environ.get("AI_WORKSPACE_TF_DIR", DEFAULT_TF_DIR)
|
||||
for loader in (
|
||||
_from_explicit_file,
|
||||
lambda: _from_default_file(tf_dir),
|
||||
):
|
||||
data = loader()
|
||||
if data:
|
||||
return data
|
||||
return {}
|
||||
|
||||
|
||||
def build_inventory(cmdb):
|
||||
inv = {"_meta": {"hostvars": {}}}
|
||||
groups = {}
|
||||
|
||||
for name, host in cmdb.items():
|
||||
hostvars = {
|
||||
"ansible_host": host.get("ip"),
|
||||
"ansible_user": host.get("ansible_user", "root"),
|
||||
# 云主机 IP 常被回收,放宽 host key 校验避免撞到旧 known_hosts
|
||||
"ansible_ssh_common_args": (
|
||||
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||
),
|
||||
}
|
||||
# CMDB 其余字段一并暴露给 playbook 使用
|
||||
hostvars.update(host.get("host_vars", {}))
|
||||
hostvars["cmdb_instance_id"] = host.get("instance_id")
|
||||
hostvars["cmdb_os_id"] = host.get("os_id")
|
||||
hostvars["cmdb_tags"] = host.get("tags", [])
|
||||
inv["_meta"]["hostvars"][name] = hostvars
|
||||
|
||||
for group in host.get("groups", []) or ["ungrouped"]:
|
||||
groups.setdefault(group, {"hosts": []})["hosts"].append(name)
|
||||
|
||||
inv.update(groups)
|
||||
inv["all"] = {"children": sorted(list(groups.keys()) + ["ungrouped"])}
|
||||
return inv
|
||||
|
||||
|
||||
def main():
|
||||
args = sys.argv[1:]
|
||||
cmdb = load_cmdb()
|
||||
|
||||
if "--host" in args:
|
||||
# hostvars 已在 _meta 里,单主机查询返回空对象即可
|
||||
print(json.dumps({}))
|
||||
return
|
||||
|
||||
# 默认与 --list 行为一致
|
||||
print(json.dumps(build_inventory(cmdb), indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
8
k3s_platform_addon.yml
Normal file
8
k3s_platform_addon.yml
Normal file
@ -0,0 +1,8 @@
|
||||
- name: Addon | single-node k3s platform
|
||||
hosts: k3s
|
||||
user: root
|
||||
become: yes
|
||||
gather_facts: yes
|
||||
tasks:
|
||||
- include_role:
|
||||
name: vhosts/k3s_platform_addon
|
||||
8
k3s_platform_bootstrap_with_gitops.yml
Normal file
8
k3s_platform_bootstrap_with_gitops.yml
Normal file
@ -0,0 +1,8 @@
|
||||
- name: Bootstrap single-node k3s GitOps platform
|
||||
hosts: k3s
|
||||
user: root
|
||||
become: yes
|
||||
gather_facts: yes
|
||||
tasks:
|
||||
- include_role:
|
||||
name: vhosts/k3s_platform_bootstrap
|
||||
10
k3s_reset.yml
Normal file
10
k3s_reset.yml
Normal file
@ -0,0 +1,10 @@
|
||||
- name: Reset single-node k3s host
|
||||
hosts: k3s
|
||||
user: root
|
||||
become: yes
|
||||
gather_facts: yes
|
||||
vars:
|
||||
cluster_reset: enable
|
||||
tasks:
|
||||
- include_role:
|
||||
name: vhosts/k3s-reset
|
||||
7
plasma_xrdp_minimal.yaml
Normal file
7
plasma_xrdp_minimal.yaml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Setup minimal Plasma + XRDP desktop
|
||||
hosts: all
|
||||
become: true
|
||||
gather_facts: true
|
||||
roles:
|
||||
- roles/vhosts/plasma_xrdp_minimal/
|
||||
95
roles/agent_skills/README.md
Normal file
95
roles/agent_skills/README.md
Normal file
@ -0,0 +1,95 @@
|
||||
# Agent Skills
|
||||
|
||||
Synchronizes controller skill sources to an Ubuntu runtime user's canonical
|
||||
skills directory, then exposes the same directory to agent-specific skill
|
||||
locations.
|
||||
|
||||
Default source and target:
|
||||
|
||||
- local marketplace source: `~/.agents/skills/`
|
||||
- local repository source: `../xworkspace-core-skills/skills/`
|
||||
- remote canonical path: `/home/ubuntu/.agents/skills/`
|
||||
- default agent targets: `codex`, `gemini`, `opencode`, `hermers`, `openclaw`
|
||||
|
||||
The repository source is categorized by capability domain, for example
|
||||
`video-production/`, `image-production/`, `animation/`, and `workspace-core/`.
|
||||
The role syncs those categories as-is, then creates root-level symlinks for
|
||||
nested skills so runtimes that scan one directory level can still discover them.
|
||||
Set `agent_skills_xworkspace_core_enabled=false` to use only the marketplace
|
||||
source, or `agent_skills_remote_flatten_nested_skills=false` to disable root
|
||||
symlink materialization.
|
||||
|
||||
The role keeps one remote source of truth and links each agent's skills entry to
|
||||
that canonical directory where the online runtime already uses links. Existing
|
||||
non-symlink target directories are rejected by default to avoid silently deleting
|
||||
agent-owned content. The live `ubuntu` Codex runtime on
|
||||
`xworkmate-bridge.svc.plus` keeps `/home/ubuntu/.codex/skills` as a real
|
||||
directory, so it is preserved by default through
|
||||
`agent_skills_preserve_existing_target_dirs`. Set
|
||||
`agent_skills_replace_existing_target_dirs=true` only when those target
|
||||
directories should be replaced.
|
||||
|
||||
Before syncing, the role can materialize the skills needed by XWorkmate typical
|
||||
scenario tests into the local canonical source. The default matrix includes:
|
||||
|
||||
| Scenario group | Skills |
|
||||
| --- | --- |
|
||||
| local document artifacts | `pptx`, `docx`, `xlsx`, `pdf` |
|
||||
| local image processing | `image-resizer` |
|
||||
| local browser automation | `browser-automation` |
|
||||
| online image generation | `image-cog` |
|
||||
| online image/video editing | `image-video-generation-editting`, `wan-image-video-generation-editting` |
|
||||
| online video translation | `video-translator` |
|
||||
| online news/search | `web-search`, `news-fetch`, `find-skills` |
|
||||
| skill maintenance | `find-skills`, `self-improving`, `skill-vetter`, `skills-security-check` |
|
||||
|
||||
Missing local skills are installed on the Ansible controller before rsync. The
|
||||
installer adapter order is:
|
||||
|
||||
1. `clawhub --workdir ~/.agents --dir skills --no-input install <skill>`
|
||||
2. `find-skills install <skill> --target ~/.agents/skills`
|
||||
|
||||
Set `agent_skills_auto_install_enabled=false` to require that all skills are
|
||||
already present locally. Set
|
||||
`agent_skills_auto_install_fail_on_missing_installer=false` to skip missing
|
||||
skills when neither installer is available; the role still fails later if a
|
||||
required skill cannot be resolved.
|
||||
|
||||
Required-skill checks search both the marketplace source and the categorized
|
||||
repository source recursively. Auto-install still writes only to
|
||||
`~/.agents/skills/`; repository-owned skills should be changed in
|
||||
`xworkspace-core-skills`.
|
||||
|
||||
After install, optional local quality gates run for each resolved skill when the
|
||||
command exists:
|
||||
|
||||
- `skill-vetter <skill_path>`
|
||||
- `skills-security-check <skill_path>`
|
||||
- `self-improving inspect <skill_path>`
|
||||
|
||||
The quality gates are enabled by default and fail the play when a present gate
|
||||
returns an error. Override `agent_skills_quality_gate_enabled=false` or
|
||||
`agent_skills_quality_gate_fail_on_error=false` only for controlled bootstrap
|
||||
environments.
|
||||
|
||||
Default sync excludes local runtime artifacts such as `.venv/`, `__pycache__/`,
|
||||
`.pyc`, and `.DS_Store`; skills should ship source, scripts, templates, and
|
||||
references rather than controller-local virtual environments.
|
||||
|
||||
The sync defaults to overlay mode (`agent_skills_delete_removed=false`) so it
|
||||
does not remove skills that already exist on the live runtime catalog. Enable
|
||||
deletion only for controlled rebuilds of `/home/ubuntu/.agents/skills/`.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills
|
||||
```
|
||||
|
||||
Bootstrap-only example that keeps the existing local source strict but skips
|
||||
quality gate failures from newly installed marketplace skills:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml --tags agent_skills \
|
||||
-e agent_skills_quality_gate_fail_on_error=false
|
||||
```
|
||||
126
roles/agent_skills/defaults/main.yml
Normal file
126
roles/agent_skills/defaults/main.yml
Normal file
@ -0,0 +1,126 @@
|
||||
---
|
||||
agent_skills_user: "{{ ansible_env.USER | default('ubuntu') }}"
|
||||
agent_skills_group: "{{ 'staff' if ansible_os_family == 'Darwin' else agent_skills_user }}"
|
||||
agent_skills_home: "{{ ansible_env.HOME | default('/home/' + agent_skills_user) }}"
|
||||
|
||||
# 规范化技能落地目录(canonical),始终在目标主机上。installer 直接装到这里,
|
||||
# core 技能 clone 后合并进来。本地/pull 与远程 controller 两种模型行为一致。
|
||||
agent_skills_remote_dir: "{{ agent_skills_home }}/.agents/skills"
|
||||
|
||||
# xworkspace-core-skills 以 git clone 获取(最通用、跨平台、双模型一致),
|
||||
# 在目标主机上 clone,不再依赖 controller 端预置目录。
|
||||
agent_skills_xworkspace_core_enabled: true
|
||||
agent_skills_xworkspace_core_required: true
|
||||
agent_skills_xworkspace_core_repo_url: "https://github.com/ai-workspace-lab/xworkspace-core-skills.git"
|
||||
agent_skills_xworkspace_core_version: "main"
|
||||
agent_skills_xworkspace_core_clone_dir: "{{ agent_skills_home }}/.local/src/xworkspace-core-skills"
|
||||
agent_skills_xworkspace_core_source_dir: "{{ agent_skills_xworkspace_core_clone_dir }}/skills"
|
||||
|
||||
agent_skills_replace_existing_target_dirs: false
|
||||
agent_skills_preserve_existing_target_dirs:
|
||||
- "{{ agent_skills_home }}/.codex/skills"
|
||||
agent_skills_remote_flatten_nested_skills: true
|
||||
agent_skills_auto_install_enabled: true
|
||||
agent_skills_auto_install_fail_on_missing_installer: true
|
||||
agent_skills_quality_gate_enabled: true
|
||||
agent_skills_quality_gate_fail_on_error: true
|
||||
agent_skills_quality_gate_commands:
|
||||
- name: skill-vetter
|
||||
argv_prefix:
|
||||
- skill-vetter
|
||||
- name: skills-security-check
|
||||
argv_prefix:
|
||||
- skills-security-check
|
||||
- name: self-improving
|
||||
argv_prefix:
|
||||
- self-improving
|
||||
- inspect
|
||||
agent_skills_typical_scenario_skills:
|
||||
- name: pptx
|
||||
scenario_groups: [local-document-artifacts]
|
||||
aliases: [pptx]
|
||||
source: local-or-clawhub
|
||||
- name: docx
|
||||
scenario_groups: [local-document-artifacts]
|
||||
aliases: [docx]
|
||||
source: local-or-clawhub
|
||||
- name: xlsx
|
||||
scenario_groups: [local-document-artifacts]
|
||||
aliases: [xlsx]
|
||||
source: local-or-clawhub
|
||||
- name: pdf
|
||||
scenario_groups: [local-document-artifacts]
|
||||
aliases: [pdf]
|
||||
source: local-or-clawhub
|
||||
- name: image-resizer
|
||||
scenario_groups: [local-image-processing]
|
||||
aliases: [image-resizer]
|
||||
source: clawhub
|
||||
- name: browser-automation
|
||||
scenario_groups: [local-browser-automation]
|
||||
aliases: [browser-automation]
|
||||
source: clawhub
|
||||
- name: image-cog
|
||||
scenario_groups: [online-image-generation]
|
||||
aliases: [image-cog]
|
||||
source: acp-descriptor
|
||||
- name: image-video-generation-editting
|
||||
scenario_groups: [online-image-video-editing]
|
||||
aliases:
|
||||
- image-video-generation-editting
|
||||
- wan-image-video-generation-editting
|
||||
source: acp-descriptor
|
||||
- name: video-translator
|
||||
scenario_groups: [online-video-translation]
|
||||
aliases: [video-translator]
|
||||
source: acp-descriptor
|
||||
- name: web-search
|
||||
scenario_groups: [online-news-fetch, online-search]
|
||||
aliases:
|
||||
- web-search
|
||||
- search
|
||||
- autoglm-websearch
|
||||
source: clawhub
|
||||
- name: news-fetch
|
||||
scenario_groups: [online-news-fetch]
|
||||
aliases:
|
||||
- news-fetch
|
||||
- blogwatcher
|
||||
source: clawhub
|
||||
- name: find-skills
|
||||
scenario_groups: [skill-maintenance, online-news-fetch, online-search]
|
||||
aliases: [find-skills]
|
||||
source: local-or-clawhub
|
||||
- name: self-improving
|
||||
scenario_groups: [skill-maintenance]
|
||||
aliases:
|
||||
- self-improving
|
||||
- self-improving-1.1.3
|
||||
source: local-or-clawhub
|
||||
- name: skill-vetter
|
||||
scenario_groups: [skill-maintenance]
|
||||
aliases:
|
||||
- skill-vetter
|
||||
- skill-vetter-1.0.0
|
||||
source: local-or-clawhub
|
||||
- name: skills-security-check
|
||||
scenario_groups: [skill-maintenance]
|
||||
aliases: [skills-security-check]
|
||||
source: local-or-clawhub
|
||||
install_force: true
|
||||
agent_skills_extra_required_skills: []
|
||||
|
||||
agent_skills_targets:
|
||||
- name: codex
|
||||
paths:
|
||||
- "{{ agent_skills_home }}/.codex/skills"
|
||||
- name: gemini
|
||||
paths:
|
||||
- "{{ agent_skills_home }}/.gemini/skills"
|
||||
- name: opencode
|
||||
paths:
|
||||
- "{{ agent_skills_home }}/.opencode/skills"
|
||||
- "{{ agent_skills_home }}/.config/opencode/skills"
|
||||
- name: openclaw
|
||||
paths:
|
||||
- "{{ agent_skills_home }}/.openclaw/skills"
|
||||
348
roles/agent_skills/tasks/main.yml
Normal file
348
roles/agent_skills/tasks/main.yml
Normal file
@ -0,0 +1,348 @@
|
||||
---
|
||||
# 设计:全程在「目标主机」上执行——没有任何 delegate_to: localhost。
|
||||
# 因此两种执行模型行为完全一致:
|
||||
# - 本地/pull:curl|bash → ansible-playbook -c local(localhost 即主机)
|
||||
# - 远程 controller:ansible-playbook -i <inventory> over ssh(任务在主机上跑)
|
||||
# 源以 git clone 获取(最通用、跨平台),不再依赖 controller 端预置目录,
|
||||
# 合并用 ansible.builtin.copy(无裸 rsync、无本地钉死)。
|
||||
|
||||
- name: Validate agent skills input
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- agent_skills_user | length > 0
|
||||
- agent_skills_group | length > 0
|
||||
- agent_skills_home | length > 0
|
||||
- agent_skills_remote_dir | length > 0
|
||||
- agent_skills_targets | length > 0
|
||||
fail_msg: "agent_skills_user/group/home, remote_dir and targets must be set."
|
||||
|
||||
- name: Build required agent skills list
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_required_entries: "{{ agent_skills_typical_scenario_skills + agent_skills_extra_required_skills }}"
|
||||
|
||||
- name: Ensure agent skills owner home exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ agent_skills_home }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
mode: "0755"
|
||||
|
||||
- name: Ensure canonical agent skills directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ agent_skills_remote_dir }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
mode: "0755"
|
||||
|
||||
# --- 源获取:在目标主机 git clone(最通用) ---------------------------------
|
||||
- name: Ensure core skills checkout parent exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ agent_skills_xworkspace_core_clone_dir | dirname }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
mode: "0755"
|
||||
when: agent_skills_xworkspace_core_enabled | bool
|
||||
|
||||
- name: Clone/update xworkspace core skills on the target host
|
||||
ansible.builtin.git:
|
||||
repo: "{{ agent_skills_xworkspace_core_repo_url }}"
|
||||
dest: "{{ agent_skills_xworkspace_core_clone_dir }}"
|
||||
version: "{{ agent_skills_xworkspace_core_version }}"
|
||||
depth: 1
|
||||
force: true
|
||||
become_user: "{{ agent_skills_user }}"
|
||||
register: agent_skills_core_clone
|
||||
when: agent_skills_xworkspace_core_enabled | bool
|
||||
|
||||
- name: Inspect core skills directory
|
||||
ansible.builtin.stat:
|
||||
path: "{{ agent_skills_xworkspace_core_source_dir }}"
|
||||
register: agent_skills_core_skills_stat
|
||||
when: agent_skills_xworkspace_core_enabled | bool
|
||||
|
||||
- name: Require core skills directory when enabled and required
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- agent_skills_core_skills_stat.stat.isdir | default(false)
|
||||
fail_msg: "core skills dir missing after clone: {{ agent_skills_xworkspace_core_source_dir }}"
|
||||
when:
|
||||
- agent_skills_xworkspace_core_enabled | bool
|
||||
- agent_skills_xworkspace_core_required | bool
|
||||
|
||||
- name: Build skill search dirs (canonical + core checkout)
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_search_dirs: >-
|
||||
{{
|
||||
[agent_skills_remote_dir]
|
||||
+ (
|
||||
(
|
||||
agent_skills_xworkspace_core_enabled | bool
|
||||
and agent_skills_core_skills_stat.stat.isdir | default(false)
|
||||
)
|
||||
| ternary([agent_skills_xworkspace_core_source_dir], [])
|
||||
)
|
||||
}}
|
||||
|
||||
# --- 缺失场景技能:用 installer 适配器装到 canonical(主机本地) --------------
|
||||
- name: Inspect required scenario skills presence
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
for d in {{ agent_skills_search_dirs | map('quote') | join(' ') }}; do
|
||||
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
|
||||
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
|
||||
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
|
||||
done
|
||||
done
|
||||
exit 1
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: agent_skills_presence
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ agent_skills_required_entries }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Build missing scenario skills list
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_missing_entries: >-
|
||||
{{ agent_skills_presence.results | selectattr('rc', 'ne', 0) | map(attribute='item') | list }}
|
||||
|
||||
- name: Install missing scenario skills via installer adapters (clawhub/find-skills)
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
skill={{ item.name | quote }}
|
||||
target_dir={{ agent_skills_remote_dir | quote }}
|
||||
parent="$(dirname "$target_dir")"; base="$(basename "$target_dir")"
|
||||
rc=1
|
||||
if command -v clawhub >/dev/null 2>&1; then
|
||||
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||
if clawhub --workdir "$parent" --dir "$base" --no-input install {{ (item.install_force | default(false) | bool) | ternary('--force', '') }} "$n"; then rc=0; break; fi
|
||||
done
|
||||
exit "$rc"
|
||||
elif command -v find-skills >/dev/null 2>&1; then
|
||||
for n in {{ ([item.install_name | default(item.name)] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||
if find-skills install "$n" --target "$target_dir"; then rc=0; break; fi
|
||||
done
|
||||
exit "$rc"
|
||||
elif [ "{{ agent_skills_auto_install_fail_on_missing_installer | bool | ternary('true', 'false') }}" = "true" ]; then
|
||||
echo "No installer (clawhub/find-skills) for $skill; preseed $target_dir/$skill." >&2
|
||||
exit 127
|
||||
else
|
||||
echo "Skipped missing $skill (no installer adapter)." >&2
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become_user: "{{ agent_skills_user }}"
|
||||
register: agent_skills_install_result
|
||||
changed_when: agent_skills_install_result.rc == 0
|
||||
loop: "{{ agent_skills_missing_entries }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
when:
|
||||
- agent_skills_auto_install_enabled | bool
|
||||
- agent_skills_missing_entries | length > 0
|
||||
|
||||
# --- 合并 core 技能到 canonical(主机本地 copy;无 rsync、无 delegate) --------
|
||||
- name: Merge core skills into canonical directory
|
||||
ansible.builtin.copy:
|
||||
src: "{{ agent_skills_xworkspace_core_source_dir }}/"
|
||||
dest: "{{ agent_skills_remote_dir }}/"
|
||||
remote_src: true
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
mode: preserve
|
||||
when:
|
||||
- agent_skills_xworkspace_core_enabled | bool
|
||||
- agent_skills_core_skills_stat.stat.isdir | default(false)
|
||||
|
||||
- name: Re-inspect required scenario skills in canonical dir
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
d={{ agent_skills_remote_dir | quote }}
|
||||
for c in {{ ([item.name] + (item.aliases | default([]))) | unique | map('quote') | join(' ') }}; do
|
||||
if [ -f "$d/$c/SKILL.md" ]; then printf '%s\n' "$d/$c"; exit 0; fi
|
||||
m="$(find "$d" -type f -path "*/$c/SKILL.md" -print -quit 2>/dev/null || true)"
|
||||
if [ -n "$m" ]; then dirname "$m"; exit 0; fi
|
||||
done
|
||||
exit 1
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: agent_skills_presence_final
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ agent_skills_required_entries }}"
|
||||
loop_control:
|
||||
label: "{{ item.name }}"
|
||||
|
||||
- name: Assert required scenario skills are available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- (agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | list | length) == 0
|
||||
fail_msg: >-
|
||||
Required scenario skills still missing under {{ agent_skills_remote_dir }}:
|
||||
{{ agent_skills_presence_final.results | selectattr('rc', 'ne', 0) | map(attribute='item.name') | join(', ') }}.
|
||||
|
||||
- name: Build resolved skill paths
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_resolved_paths: >-
|
||||
{{ agent_skills_presence_final.results | selectattr('rc', 'eq', 0) | map(attribute='stdout') | list | unique }}
|
||||
|
||||
- name: Run optional scenario skill quality gates
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
skill_path={{ item.0 | quote }}
|
||||
gate_name={{ item.1.name | quote }}
|
||||
if command -v "$gate_name" >/dev/null 2>&1; then
|
||||
{{ item.1.argv_prefix | map('quote') | join(' ') }} "$skill_path"
|
||||
else
|
||||
echo "Skipped missing quality gate: $gate_name"
|
||||
fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
become_user: "{{ agent_skills_user }}"
|
||||
register: agent_skills_quality_gate_results
|
||||
changed_when: false
|
||||
failed_when: agent_skills_quality_gate_fail_on_error | bool and agent_skills_quality_gate_results.rc != 0
|
||||
loop: "{{ agent_skills_resolved_paths | product(agent_skills_quality_gate_commands) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.1.name }} {{ item.0 | basename }}"
|
||||
when:
|
||||
- agent_skills_quality_gate_enabled | bool
|
||||
- agent_skills_resolved_paths | length > 0
|
||||
check_mode: false
|
||||
|
||||
- name: Set canonical agent skills ownership
|
||||
ansible.builtin.file:
|
||||
path: "{{ agent_skills_remote_dir }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
recurse: true
|
||||
|
||||
# --- 把分类嵌套技能在 canonical 根做扁平 symlink(主机本地) ------------------
|
||||
- name: Link nested categorized skills at canonical root
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
changed=0
|
||||
while IFS= read -r skill_manifest; do
|
||||
skill_dir="$(dirname "$skill_manifest")"
|
||||
skill_name="$(basename "$skill_dir")"
|
||||
link_path={{ agent_skills_remote_dir | quote }}/"$skill_name"
|
||||
if [ -e "$link_path" ] && [ ! -L "$link_path" ]; then continue; fi
|
||||
current_target=""
|
||||
if [ -L "$link_path" ]; then current_target="$(readlink "$link_path")"; fi
|
||||
if [ "$current_target" != "$skill_dir" ]; then
|
||||
if [ "{{ ansible_check_mode | ternary('true', 'false') }}" != "true" ]; then
|
||||
ln -sfn "$skill_dir" "$link_path"
|
||||
fi
|
||||
changed=1
|
||||
fi
|
||||
done < <(find {{ agent_skills_remote_dir | quote }} -mindepth 3 -name SKILL.md -type f -print)
|
||||
if [ "$changed" = "1" ]; then echo "<<CHANGED>>linked nested skills"; fi
|
||||
args:
|
||||
executable: /bin/bash
|
||||
register: agent_skills_flatten_result
|
||||
changed_when: "'<<CHANGED>>' in agent_skills_flatten_result.stdout"
|
||||
check_mode: false
|
||||
when: agent_skills_remote_flatten_nested_skills | bool
|
||||
|
||||
- name: Set canonical agent skills ownership after nested links
|
||||
ansible.builtin.file:
|
||||
path: "{{ agent_skills_remote_dir }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
recurse: true
|
||||
when: agent_skills_remote_flatten_nested_skills | bool
|
||||
|
||||
# --- 把各 Agent 的 skills 目录 symlink 到 canonical ---------------------------
|
||||
- name: Flatten agent skills target paths
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_target_paths: "{{ agent_skills_targets | subelements('paths') | map('last') | list }}"
|
||||
|
||||
- name: Inspect agent skills target paths
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
register: agent_skills_target_path_stats
|
||||
loop: "{{ agent_skills_target_paths }}"
|
||||
|
||||
- name: Reject existing non-link target directories unless replacement is enabled
|
||||
ansible.builtin.fail:
|
||||
msg: >-
|
||||
Agent skills target already exists and is not a symlink: {{ item.item }}.
|
||||
Set agent_skills_replace_existing_target_dirs=true to replace it with a link
|
||||
to {{ agent_skills_remote_dir }}.
|
||||
loop: "{{ agent_skills_target_path_stats.results }}"
|
||||
when:
|
||||
- item.stat.exists | default(false)
|
||||
- not item.stat.islnk | default(false)
|
||||
- item.item not in agent_skills_preserve_existing_target_dirs
|
||||
- not agent_skills_replace_existing_target_dirs | bool
|
||||
|
||||
- name: Replace existing non-link target directories when enabled
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.item }}"
|
||||
state: absent
|
||||
loop: "{{ agent_skills_target_path_stats.results }}"
|
||||
when:
|
||||
- item.stat.exists | default(false)
|
||||
- not item.stat.islnk | default(false)
|
||||
- item.item not in agent_skills_preserve_existing_target_dirs
|
||||
- agent_skills_replace_existing_target_dirs | bool
|
||||
|
||||
- name: Build agent skills target parent paths
|
||||
ansible.builtin.set_fact:
|
||||
agent_skills_target_parent_paths: "{{ agent_skills_target_paths | map('dirname') | list | unique }}"
|
||||
|
||||
- name: Inspect agent skills target parent directories
|
||||
ansible.builtin.stat:
|
||||
path: "{{ item }}"
|
||||
register: agent_skills_target_parent_stats
|
||||
loop: "{{ agent_skills_target_parent_paths }}"
|
||||
|
||||
- name: Ensure agent skills target parent directories exist
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.item }}"
|
||||
state: directory
|
||||
owner: "{{ agent_skills_user }}"
|
||||
group: "{{ agent_skills_group }}"
|
||||
mode: "0755"
|
||||
loop: "{{ agent_skills_target_parent_stats.results }}"
|
||||
when:
|
||||
- not item.stat.exists | default(false)
|
||||
|
||||
- name: Link agent skills targets to canonical directory
|
||||
ansible.builtin.file:
|
||||
src: "{{ agent_skills_remote_dir }}"
|
||||
dest: "{{ item }}"
|
||||
state: link
|
||||
force: true
|
||||
loop: "{{ agent_skills_target_paths }}"
|
||||
when: item not in agent_skills_preserve_existing_target_dirs
|
||||
|
||||
- name: Verify canonical skill manifests are present
|
||||
ansible.builtin.find:
|
||||
paths: "{{ agent_skills_remote_dir }}"
|
||||
patterns: SKILL.md
|
||||
recurse: true
|
||||
file_type: file
|
||||
register: agent_skills_manifest_files
|
||||
|
||||
- name: Assert synced agent skills contain manifests
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- agent_skills_manifest_files.matched | int > 0
|
||||
fail_msg: "No SKILL.md files found under {{ agent_skills_remote_dir }}."
|
||||
|
||||
- name: Report synced agent skills
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
{{ agent_skills_manifest_files.matched }} skill manifests under
|
||||
{{ agent_skills_remote_dir }}; linked {{ agent_skills_target_paths | length }} agent targets.
|
||||
48
roles/ai_agent_runtime/README.md
Normal file
48
roles/ai_agent_runtime/README.md
Normal file
@ -0,0 +1,48 @@
|
||||
# AI Agent Runtime
|
||||
|
||||
Provision a Debian-based host for AI agent and AI action execution with one
|
||||
role entrypoint. The role installs:
|
||||
|
||||
- base tools: `curl`, `wget`, `git`, `jq`, `rsync`, `unzip`
|
||||
- Node.js runtime for Playwright-based agents
|
||||
- Python 3 toolchain for scripts and helpers
|
||||
- existing system browser, preferring the live `/usr/local/bin/chromium` wrapper
|
||||
or Google Chrome before installing browser packages
|
||||
- `pandoc` + XeLaTeX PDF toolchain
|
||||
- Chinese fonts for document rendering
|
||||
- shared agent skills via `roles/agent_skills`, including the categorized
|
||||
`../xworkspace-core-skills/skills/` repository source by default
|
||||
|
||||
Design constraints:
|
||||
|
||||
- system packages are the primary source of truth
|
||||
- global npm packages are managed through
|
||||
`/usr/local/sbin/ai-workspace-manage-npm-global-package` so repeated installs
|
||||
are idempotent and stale global bin links can be overwritten safely
|
||||
- Playwright uses the resolved system browser instead of downloading browsers
|
||||
- Chinese PDF rendering is treated as a runtime requirement, not an optional add-on
|
||||
|
||||
Global npm package actions:
|
||||
|
||||
- `install` is the default and only changes the host when a package is missing
|
||||
or an exact pinned version differs
|
||||
- `reinstall` forces the configured package set back into place
|
||||
- `upgrade`, `backup`, `restore`, and `migrate` are reserved action entrypoints
|
||||
for future runtime lifecycle workflows
|
||||
|
||||
Default Playwright environment:
|
||||
|
||||
- `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1`
|
||||
- `PLAYWRIGHT_BROWSERS_PATH=0`
|
||||
- `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=/usr/local/bin/chromium` when that live
|
||||
wrapper exists
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
ansible-playbook -i inventory.ini -l jp-xhttp-contabo.svc.plus setup-ai-agent-skills.yml
|
||||
```
|
||||
|
||||
`setup-ai-agent-skills.yml` runs `roles/ai_agent_runtime`, which installs system
|
||||
dependencies and syncs the current Skill catalog through the embedded
|
||||
`roles/agent_skills` step in one pass.
|
||||
66
roles/ai_agent_runtime/defaults/main.yml
Normal file
66
roles/ai_agent_runtime/defaults/main.yml
Normal file
@ -0,0 +1,66 @@
|
||||
---
|
||||
ai_agent_runtime_base_packages:
|
||||
- ca-certificates
|
||||
- curl
|
||||
- git
|
||||
- jq
|
||||
- rsync
|
||||
- unzip
|
||||
- wget
|
||||
|
||||
ai_agent_runtime_nodejs_enabled: true
|
||||
ai_agent_runtime_nodejs_version: "24.16.0"
|
||||
ai_agent_runtime_install_yarn: true
|
||||
ai_agent_runtime_yarn_version: ""
|
||||
ai_agent_runtime_npm_global_packages:
|
||||
- opencode-ai
|
||||
- "@google/gemini-cli"
|
||||
- "@openai/codex"
|
||||
- "@anthropic-ai/claude-code"
|
||||
ai_agent_runtime_npm_global_package_action: install
|
||||
ai_agent_runtime_agent_cli_commands:
|
||||
- opencode
|
||||
- gemini
|
||||
- codex
|
||||
- claude
|
||||
ai_agent_runtime_playwright_enabled: true
|
||||
ai_agent_runtime_playwright_version: "1.60.0"
|
||||
ai_agent_runtime_playwright_skip_browser_download: true
|
||||
|
||||
ai_agent_runtime_python_enabled: true
|
||||
ai_agent_runtime_python_packages:
|
||||
- python3
|
||||
- python3-pip
|
||||
- python3-venv
|
||||
- python3-dev
|
||||
- python3-setuptools
|
||||
- build-essential
|
||||
- pkg-config
|
||||
- python-is-python3
|
||||
|
||||
ai_agent_runtime_browser_enabled: true
|
||||
ai_agent_runtime_browser_packages:
|
||||
- google-chrome-stable
|
||||
ai_agent_runtime_browser_executable: /usr/local/bin/chromium
|
||||
|
||||
ai_agent_runtime_docs_enabled: true
|
||||
ai_agent_runtime_doc_packages:
|
||||
- pandoc
|
||||
- texlive-xetex
|
||||
- texlive-latex-extra
|
||||
- texlive-fonts-recommended
|
||||
- texlive-lang-chinese
|
||||
- latexmk
|
||||
|
||||
ai_agent_runtime_fonts_enabled: true
|
||||
ai_agent_runtime_font_packages:
|
||||
- fonts-noto-cjk
|
||||
- fonts-noto-cjk-extra
|
||||
- fonts-wqy-zenhei
|
||||
- fonts-wqy-microhei
|
||||
|
||||
ai_agent_runtime_skills_enabled: true
|
||||
ai_agent_runtime_skills_role_name: agent_skills
|
||||
|
||||
ai_agent_runtime_verify_enabled: true
|
||||
ai_agent_runtime_verify_chinese_fonts: true
|
||||
113
roles/ai_agent_runtime/files/manage_npm_global_package.sh
Normal file
113
roles/ai_agent_runtime/files/manage_npm_global_package.sh
Normal file
@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
action="${1:-install}"
|
||||
package_spec="${2:-}"
|
||||
|
||||
if [ -z "${package_spec}" ]; then
|
||||
echo "Usage: $0 <install|reinstall|upgrade|backup|restore|migrate> <npm-package-spec>" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
package_name() {
|
||||
local spec="$1"
|
||||
if [[ "${spec}" == @* ]]; then
|
||||
local rest="${spec#@}"
|
||||
local scope="${rest%%/*}"
|
||||
local after_scope="${rest#*/}"
|
||||
local name="${after_scope%%@*}"
|
||||
printf '@%s/%s\n' "${scope}" "${name}"
|
||||
else
|
||||
printf '%s\n' "${spec%%@*}"
|
||||
fi
|
||||
}
|
||||
|
||||
desired_version() {
|
||||
local spec="$1"
|
||||
if [[ "${spec}" == @* ]]; then
|
||||
local rest="${spec#@}"
|
||||
local after_scope="${rest#*/}"
|
||||
if [[ "${after_scope}" == *"@"* ]]; then
|
||||
printf '%s\n' "${after_scope#*@}"
|
||||
fi
|
||||
elif [[ "${spec}" == *"@"* ]]; then
|
||||
printf '%s\n' "${spec#*@}"
|
||||
fi
|
||||
}
|
||||
|
||||
installed_version() {
|
||||
local name="$1"
|
||||
local npm_root
|
||||
npm_root="$(npm root -g)"
|
||||
node -e '
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const pkg = process.argv[1];
|
||||
const root = process.argv[2];
|
||||
const packageJson = path.join(root, ...pkg.split("/"), "package.json");
|
||||
if (!fs.existsSync(packageJson)) process.exit(1);
|
||||
const parsed = JSON.parse(fs.readFileSync(packageJson, "utf8"));
|
||||
process.stdout.write(parsed.version || "");
|
||||
' "${name}" "${npm_root}"
|
||||
}
|
||||
|
||||
is_installed() {
|
||||
local name="$1"
|
||||
local want="${2:-}"
|
||||
local have
|
||||
have="$(installed_version "${name}" 2>/dev/null || true)"
|
||||
[ -n "${have}" ] || return 1
|
||||
[ -z "${want}" ] || [ "${have}" = "${want}" ]
|
||||
}
|
||||
|
||||
install_package() {
|
||||
local spec="$1"
|
||||
local name want
|
||||
name="$(package_name "${spec}")"
|
||||
want="$(desired_version "${spec}")"
|
||||
|
||||
if is_installed "${name}" "${want}"; then
|
||||
echo "changed=0 action=install package=${spec}"
|
||||
return
|
||||
fi
|
||||
|
||||
npm install -g --force "${spec}"
|
||||
echo "changed=1 action=install package=${spec}"
|
||||
}
|
||||
|
||||
reinstall_package() {
|
||||
local spec="$1"
|
||||
npm install -g --force "${spec}"
|
||||
echo "changed=1 action=reinstall package=${spec}"
|
||||
}
|
||||
|
||||
upgrade_package() {
|
||||
local spec="$1"
|
||||
npm install -g --force "${spec}"
|
||||
echo "changed=1 action=upgrade package=${spec}"
|
||||
}
|
||||
|
||||
backup_package() {
|
||||
echo "changed=0 action=backup package=${1} status=reserved"
|
||||
}
|
||||
|
||||
restore_package() {
|
||||
echo "changed=0 action=restore package=${1} status=reserved"
|
||||
}
|
||||
|
||||
migrate_package() {
|
||||
echo "changed=0 action=migrate package=${1} status=reserved"
|
||||
}
|
||||
|
||||
case "${action}" in
|
||||
install) install_package "${package_spec}" ;;
|
||||
reinstall) reinstall_package "${package_spec}" ;;
|
||||
upgrade) upgrade_package "${package_spec}" ;;
|
||||
backup) backup_package "${package_spec}" ;;
|
||||
restore) restore_package "${package_spec}" ;;
|
||||
migrate) migrate_package "${package_spec}" ;;
|
||||
*)
|
||||
echo "Unsupported npm package action: ${action}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
111
roles/ai_agent_runtime/tasks/browser.yml
Normal file
111
roles/ai_agent_runtime/tasks/browser.yml
Normal file
@ -0,0 +1,111 @@
|
||||
---
|
||||
- name: Resolve existing Chromium executable
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
for candidate in \
|
||||
"{{ ai_agent_runtime_browser_executable }}" \
|
||||
/usr/local/bin/chromium \
|
||||
/usr/local/bin/chromium-browser \
|
||||
/usr/bin/google-chrome \
|
||||
/usr/bin/google-chrome-stable \
|
||||
chromium \
|
||||
chromium-browser \
|
||||
google-chrome \
|
||||
google-chrome-stable \
|
||||
/usr/bin/chromium \
|
||||
/snap/bin/chromium; do
|
||||
resolved=""
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
resolved="$(command -v "$candidate")"
|
||||
elif [ -x "$candidate" ]; then
|
||||
resolved="$candidate"
|
||||
fi
|
||||
# 必须真正可执行:跳过 xfce 安装的 disabled chromium stub(退出码 126),
|
||||
# 否则 resolver 会选中它,后续 --version 校验必失败。
|
||||
if [ -n "$resolved" ] && "$resolved" --version >/dev/null 2>&1; then
|
||||
printf '%s\n' "$resolved"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
args:
|
||||
executable: /bin/sh
|
||||
register: ai_agent_runtime_browser_resolve
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Install AI runtime browser packages when no browser exists
|
||||
ansible.builtin.apt:
|
||||
name: "{{ ai_agent_runtime_browser_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
install_recommends: false
|
||||
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
become: true
|
||||
when:
|
||||
- ai_agent_runtime_browser_resolve.rc != 0
|
||||
- ansible_os_family != 'Darwin'
|
||||
|
||||
- name: Resolve Chromium executable
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
for candidate in \
|
||||
"{{ ai_agent_runtime_browser_executable }}" \
|
||||
/usr/local/bin/chromium \
|
||||
/usr/local/bin/chromium-browser \
|
||||
/usr/bin/google-chrome \
|
||||
/usr/bin/google-chrome-stable \
|
||||
chromium \
|
||||
chromium-browser \
|
||||
google-chrome-stable \
|
||||
/usr/bin/chromium \
|
||||
/snap/bin/chromium \
|
||||
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; do
|
||||
resolved=""
|
||||
if command -v "$candidate" >/dev/null 2>&1; then
|
||||
resolved="$(command -v "$candidate")"
|
||||
elif [ -x "$candidate" ]; then
|
||||
resolved="$candidate"
|
||||
fi
|
||||
# 必须真正可执行:跳过 xfce 安装的 disabled chromium stub(退出码 126),
|
||||
# 否则 resolver 会选中它,后续 --version 校验必失败。
|
||||
if [ -n "$resolved" ] && "$resolved" --version >/dev/null 2>&1; then
|
||||
printf '%s\n' "$resolved"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
exit 1
|
||||
args:
|
||||
executable: /bin/sh
|
||||
register: ai_agent_runtime_browser_resolve
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
|
||||
- name: Set resolved Chromium executable
|
||||
ansible.builtin.set_fact:
|
||||
ai_agent_runtime_browser_resolved_executable: "{{ ai_agent_runtime_browser_resolve.stdout }}"
|
||||
|
||||
- name: Ensure AI workspace env directory exists on macOS
|
||||
ansible.builtin.file:
|
||||
path: "{{ xworkspace_console_home | default(ansible_env.HOME) }}/.local/state/ai-workspace/env"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: ansible_os_family == 'Darwin'
|
||||
|
||||
- name: Configure Playwright runtime environment
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ '/etc/profile.d' if ansible_os_family != 'Darwin' else (xworkspace_console_home | default(ansible_env.HOME)) + '/.local/state/ai-workspace/env' }}/ai_agent_runtime_playwright.sh"
|
||||
owner: "{{ 'root' if ansible_os_family != 'Darwin' else (xworkspace_console_user | default(ansible_env.USER)) }}"
|
||||
group: "{{ 'root' if ansible_os_family != 'Darwin' else ('staff' if ansible_os_family == 'Darwin' else (xworkspace_console_user | default(ansible_env.USER))) }}"
|
||||
mode: "0644"
|
||||
content: |
|
||||
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD={{ '1' if ai_agent_runtime_playwright_skip_browser_download | bool else '0' }}
|
||||
export PLAYWRIGHT_BROWSERS_PATH=0
|
||||
export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH={{ ai_agent_runtime_browser_resolved_executable }}
|
||||
become: "{{ ansible_os_family != 'Darwin' }}"
|
||||
when: ai_agent_runtime_playwright_enabled | bool
|
||||
14
roles/ai_agent_runtime/tasks/docs.yml
Normal file
14
roles/ai_agent_runtime/tasks/docs.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Install AI runtime document packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ ai_agent_runtime_doc_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
install_recommends: false
|
||||
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
become: true
|
||||
when: ansible_os_family != 'Darwin'
|
||||
14
roles/ai_agent_runtime/tasks/fonts.yml
Normal file
14
roles/ai_agent_runtime/tasks/fonts.yml
Normal file
@ -0,0 +1,14 @@
|
||||
---
|
||||
- name: Install AI runtime font packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ ai_agent_runtime_font_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
install_recommends: false
|
||||
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
become: true
|
||||
when: ansible_os_family != 'Darwin'
|
||||
52
roles/ai_agent_runtime/tasks/main.yml
Normal file
52
roles/ai_agent_runtime/tasks/main.yml
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
- name: Assert AI agent runtime is supported on Debian or Darwin family
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ansible_facts.os_family in ["Debian", "Darwin", "Windows"]
|
||||
fail_msg: "roles/ai_agent_runtime currently supports Debian-based, Darwin, and Windows hosts only."
|
||||
|
||||
- name: Install AI agent runtime base packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ ai_agent_runtime_base_packages }}"
|
||||
state: present
|
||||
update_cache: true
|
||||
install_recommends: false
|
||||
# 等 dpkg 前端锁,避免与 cloud-init/unattended-upgrades 抢锁而立即失败
|
||||
lock_timeout: "{{ ai_workspace_apt_lock_timeout | default(900) | int }}"
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
APT_LISTCHANGES_FRONTEND: none
|
||||
become: true
|
||||
when: ansible_os_family == 'Debian'
|
||||
|
||||
- name: Configure Node.js runtime
|
||||
ansible.builtin.include_tasks: nodejs.yml
|
||||
when: ai_agent_runtime_nodejs_enabled | bool
|
||||
|
||||
- name: Configure Python runtime
|
||||
ansible.builtin.include_tasks: python.yml
|
||||
when: ai_agent_runtime_python_enabled | bool
|
||||
|
||||
- name: Configure browser runtime
|
||||
ansible.builtin.include_tasks: browser.yml
|
||||
when: ai_agent_runtime_browser_enabled | bool
|
||||
|
||||
- name: Configure document runtime
|
||||
ansible.builtin.include_tasks: docs.yml
|
||||
when: ai_agent_runtime_docs_enabled | bool
|
||||
|
||||
- name: Configure font runtime
|
||||
ansible.builtin.include_tasks: fonts.yml
|
||||
when: ai_agent_runtime_fonts_enabled | bool
|
||||
|
||||
- name: Configure shared agent skills
|
||||
ansible.builtin.include_role:
|
||||
name: "{{ ai_agent_runtime_skills_role_name }}"
|
||||
apply:
|
||||
tags: agent_skills
|
||||
when: ai_agent_runtime_skills_enabled | bool
|
||||
tags: [agent_skills]
|
||||
|
||||
- name: Verify AI agent runtime
|
||||
ansible.builtin.include_tasks: verify.yml
|
||||
when: ai_agent_runtime_verify_enabled | bool
|
||||
49
roles/ai_agent_runtime/tasks/nodejs.yml
Normal file
49
roles/ai_agent_runtime/tasks/nodejs.yml
Normal file
@ -0,0 +1,49 @@
|
||||
---
|
||||
- name: Install Node.js role
|
||||
ansible.builtin.include_role:
|
||||
name: roles/vhosts/nodejs
|
||||
vars:
|
||||
nodejs_version: "{{ ai_agent_runtime_nodejs_version }}"
|
||||
install_yarn: "{{ ai_agent_runtime_install_yarn }}"
|
||||
yarn_version: "{{ ai_agent_runtime_yarn_version }}"
|
||||
|
||||
- name: Ensure user local bin directory exists on macOS
|
||||
ansible.builtin.file:
|
||||
path: "{{ ansible_env.HOME }}/.local/bin"
|
||||
state: directory
|
||||
mode: "0755"
|
||||
when: ansible_os_family == 'Darwin'
|
||||
|
||||
- name: Install npm global package manager helper
|
||||
ansible.builtin.copy:
|
||||
src: manage_npm_global_package.sh
|
||||
dest: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package"
|
||||
owner: "{{ 'root' if ansible_os_family != 'Darwin' else xworkspace_console_user }}"
|
||||
group: "{{ 'root' if ansible_os_family != 'Darwin' else ('staff' if ansible_os_family == 'Darwin' else xworkspace_console_user) }}"
|
||||
mode: "0755"
|
||||
become: "{{ ansible_os_family != 'Darwin' }}"
|
||||
when: ansible_os_family != 'Windows'
|
||||
|
||||
- name: Install global npm packages for AI runtime
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} {{ item }}"
|
||||
loop: "{{ ai_agent_runtime_npm_global_packages }}"
|
||||
register: ai_agent_runtime_npm_global_install
|
||||
changed_when: "'changed=1' in ai_agent_runtime_npm_global_install.stdout"
|
||||
when:
|
||||
- ai_agent_runtime_npm_global_packages | length > 0
|
||||
- ansible_os_family != 'Windows'
|
||||
|
||||
- name: Install pinned Playwright package for AI runtime
|
||||
ansible.builtin.command:
|
||||
cmd: "{{ '/usr/local/sbin' if ansible_os_family != 'Darwin' else ansible_env.HOME + '/.local/bin' }}/ai-workspace-manage-npm-global-package {{ ai_agent_runtime_npm_global_package_action }} playwright@{{ ai_agent_runtime_playwright_version }}"
|
||||
register: ai_agent_runtime_playwright_install
|
||||
changed_when: "'changed=1' in ai_agent_runtime_playwright_install.stdout"
|
||||
when:
|
||||
- ai_agent_runtime_playwright_enabled | bool
|
||||
- ai_agent_runtime_playwright_version | length > 0
|
||||
- ansible_os_family != 'Windows'
|
||||
|
||||
- name: Include Windows specific Node.js package tasks
|
||||
ansible.builtin.include_tasks: windows.yml
|
||||
when: ansible_os_family == 'Windows'
|
||||
7
roles/ai_agent_runtime/tasks/python.yml
Normal file
7
roles/ai_agent_runtime/tasks/python.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Install Python 3 role
|
||||
ansible.builtin.include_role:
|
||||
name: roles/vhosts/python3
|
||||
vars:
|
||||
python3_packages: "{{ ai_agent_runtime_python_packages }}"
|
||||
python3_install_recommends: false
|
||||
98
roles/ai_agent_runtime/tasks/verify.yml
Normal file
98
roles/ai_agent_runtime/tasks/verify.yml
Normal file
@ -0,0 +1,98 @@
|
||||
---
|
||||
- name: Check node version
|
||||
ansible.builtin.command: node --version
|
||||
register: ai_agent_runtime_node_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_nodejs_enabled | bool
|
||||
|
||||
- name: Check npm version
|
||||
ansible.builtin.command: npm --version
|
||||
register: ai_agent_runtime_npm_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_nodejs_enabled | bool
|
||||
|
||||
- name: Check python version
|
||||
ansible.builtin.command: python3 --version
|
||||
register: ai_agent_runtime_python_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_python_enabled | bool
|
||||
|
||||
- name: Check pip version
|
||||
ansible.builtin.command: pip3 --version
|
||||
register: ai_agent_runtime_pip_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_python_enabled | bool
|
||||
|
||||
- name: Check chromium version
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- "{{ ai_agent_runtime_browser_resolved_executable | default(ai_agent_runtime_browser_executable) }}"
|
||||
- "--version"
|
||||
register: ai_agent_runtime_chromium_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_browser_enabled | bool
|
||||
|
||||
- name: Check pandoc version
|
||||
ansible.builtin.command: pandoc --version
|
||||
register: ai_agent_runtime_pandoc_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_docs_enabled | bool
|
||||
|
||||
- name: Check xelatex version
|
||||
ansible.builtin.command: xelatex --version
|
||||
register: ai_agent_runtime_xelatex_version
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when: ai_agent_runtime_docs_enabled | bool
|
||||
|
||||
- name: Check Chinese font inventory
|
||||
ansible.builtin.command: fc-list :lang=zh family
|
||||
register: ai_agent_runtime_chinese_fonts
|
||||
changed_when: false
|
||||
check_mode: false
|
||||
when:
|
||||
- ai_agent_runtime_fonts_enabled | bool
|
||||
- ai_agent_runtime_verify_chinese_fonts | bool
|
||||
|
||||
- name: Assert Chinese fonts are available
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- ai_agent_runtime_chinese_fonts.stdout | length > 0
|
||||
fail_msg: "No Chinese fonts were discovered by fontconfig."
|
||||
when:
|
||||
- ai_agent_runtime_fonts_enabled | bool
|
||||
- ai_agent_runtime_verify_chinese_fonts | bool
|
||||
|
||||
- name: Check agent CLI versions
|
||||
ansible.builtin.command: "{{ item }} --version"
|
||||
register: ai_agent_runtime_agent_cli_versions
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
check_mode: false
|
||||
loop: "{{ ai_agent_runtime_agent_cli_commands }}"
|
||||
when:
|
||||
- ai_agent_runtime_nodejs_enabled | bool
|
||||
- ai_agent_runtime_agent_cli_commands | length > 0
|
||||
|
||||
- name: Report AI runtime versions
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
node: "{{ ai_agent_runtime_node_version.stdout | default('disabled') }}"
|
||||
npm: "{{ ai_agent_runtime_npm_version.stdout | default('disabled') }}"
|
||||
python3: "{{ ai_agent_runtime_python_version.stdout | default('disabled') }}"
|
||||
pip3: "{{ ai_agent_runtime_pip_version.stdout | default('disabled') }}"
|
||||
chromium: "{{ ai_agent_runtime_chromium_version.stdout | default('disabled') }}"
|
||||
pandoc: "{{ (ai_agent_runtime_pandoc_version.stdout_lines | default(['disabled']))[0] }}"
|
||||
xelatex: "{{ (ai_agent_runtime_xelatex_version.stdout_lines | default(['disabled']))[0] }}"
|
||||
chinese_font_count: "{{ (ai_agent_runtime_chinese_fonts.stdout_lines | default([])) | length }}"
|
||||
agent_cli: >-
|
||||
{{
|
||||
ai_agent_runtime_agent_cli_versions.results | default([])
|
||||
| items2dict(key_name='item', value_name='stdout')
|
||||
}}
|
||||
17
roles/ai_agent_runtime/tasks/windows.yml
Normal file
17
roles/ai_agent_runtime/tasks/windows.yml
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
- name: Install global npm packages for AI runtime on Windows
|
||||
community.windows.win_command:
|
||||
cmd: "npm install -g {{ item }}"
|
||||
loop: "{{ ai_agent_runtime_npm_global_packages }}"
|
||||
register: ai_agent_runtime_npm_global_install_win
|
||||
changed_when: "'added' in ai_agent_runtime_npm_global_install_win.stdout or 'updated' in ai_agent_runtime_npm_global_install_win.stdout"
|
||||
when: ai_agent_runtime_npm_global_packages | length > 0
|
||||
|
||||
- name: Install pinned Playwright package for AI runtime on Windows
|
||||
community.windows.win_command:
|
||||
cmd: "npm install -g playwright@{{ ai_agent_runtime_playwright_version }}"
|
||||
register: ai_agent_runtime_playwright_install_win
|
||||
changed_when: "'added' in ai_agent_runtime_playwright_install_win.stdout or 'updated' in ai_agent_runtime_playwright_install_win.stdout"
|
||||
when:
|
||||
- ai_agent_runtime_playwright_enabled | bool
|
||||
- ai_agent_runtime_playwright_version | length > 0
|
||||
232
roles/azure_dev_desktop_lifecycle/tasks/create.yml
Normal file
232
roles/azure_dev_desktop_lifecycle/tasks/create.yml
Normal file
@ -0,0 +1,232 @@
|
||||
- name: Preview Azure create request
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "azure_resource_group={{ azure_resource_group }}"
|
||||
- "azure_vm_name={{ azure_vm_name }}"
|
||||
- "azure_computer_name={{ azure_computer_name }}"
|
||||
- "azure_location={{ azure_location }}"
|
||||
- "allowed_cidrs={{ allowed_cidrs | join(',') }}"
|
||||
- "allowed_tcp_ports={{ allowed_tcp_ports | join(',') }}"
|
||||
- "state_file={{ cloud_vm_state_file }}"
|
||||
|
||||
- name: Ensure Azure resource group exists
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- group
|
||||
- create
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --name
|
||||
- "{{ azure_resource_group }}"
|
||||
- --location
|
||||
- "{{ azure_location }}"
|
||||
- --tags
|
||||
- "{{ tags | dict2items | map('join', '=') | join(' ') }}"
|
||||
changed_when: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Ensure Azure virtual network exists
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- network
|
||||
- vnet
|
||||
- create
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_virtual_network }}"
|
||||
- --address-prefixes
|
||||
- "{{ azure_vnet_cidr | default('10.42.0.0/16') }}"
|
||||
- --subnet-name
|
||||
- "{{ azure_subnet }}"
|
||||
- --subnet-prefixes
|
||||
- "{{ azure_subnet_cidr | default('10.42.1.0/24') }}"
|
||||
changed_when: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Ensure Azure network security group exists
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- network
|
||||
- nsg
|
||||
- create
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_network_security_group }}"
|
||||
- --location
|
||||
- "{{ azure_location }}"
|
||||
changed_when: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Create Azure allowlist rules
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- network
|
||||
- nsg
|
||||
- rule
|
||||
- create
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --nsg-name
|
||||
- "{{ azure_network_security_group }}"
|
||||
- --name
|
||||
- "allow-tcp-{{ port }}-{{ '%03d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
|
||||
- --priority
|
||||
- "{{ '%d' | format((index | int) + ((port_index | int) * 100) + 100) }}"
|
||||
- --access
|
||||
- Allow
|
||||
- --protocol
|
||||
- Tcp
|
||||
- --direction
|
||||
- Inbound
|
||||
- --source-address-prefixes
|
||||
- "{{ cidr }}"
|
||||
- --source-port-ranges
|
||||
- "*"
|
||||
- --destination-port-ranges
|
||||
- "{{ port | string }}"
|
||||
loop: "{{ allowed_cidrs | product(allowed_tcp_ports) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.0 }} -> {{ item.1 }}"
|
||||
index_var: combo_index
|
||||
changed_when: true
|
||||
when: not ansible_check_mode
|
||||
vars:
|
||||
cidr: "{{ item.0 }}"
|
||||
port: "{{ item.1 }}"
|
||||
index: "{{ combo_index % (allowed_cidrs | length) }}"
|
||||
port_index: "{{ combo_index // (allowed_cidrs | length) }}"
|
||||
|
||||
- name: Build Azure VM create command
|
||||
ansible.builtin.set_fact:
|
||||
azure_vm_create_command: >-
|
||||
az vm create
|
||||
--subscription {{ azure_subscription_id | quote }}
|
||||
--resource-group {{ azure_resource_group | quote }}
|
||||
--name {{ azure_vm_name | quote }}
|
||||
--computer-name {{ azure_computer_name | quote }}
|
||||
--image {{ (azure_image_publisher ~ ':' ~ azure_image_offer ~ ':' ~ azure_image_sku ~ ':' ~ azure_image_version) | quote }}
|
||||
--size {{ vm_size | quote }}
|
||||
--admin-username {{ admin_username | quote }}
|
||||
--vnet-name {{ azure_virtual_network | quote }}
|
||||
--subnet {{ azure_subnet | quote }}
|
||||
--nsg {{ azure_network_security_group | quote }}
|
||||
--public-ip-sku Standard
|
||||
--public-ip-address {{ azure_public_ip_name | quote }}
|
||||
--storage-sku Premium_LRS
|
||||
--os-disk-size-gb {{ disk_gb | int }}
|
||||
--tags {{ tags | dict2items | map('join', '=') | join(' ') | quote }}
|
||||
{% if os_family == 'windows' %}
|
||||
--admin-password {{ azure_admin_password | quote }}
|
||||
{% else %}
|
||||
--ssh-key-values {{ ssh_public_key_path | quote }}
|
||||
{% endif %}
|
||||
|
||||
- name: Create Azure VM
|
||||
ansible.builtin.shell: "{{ azure_vm_create_command }}"
|
||||
args:
|
||||
executable: /bin/bash
|
||||
changed_when: true
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Fetch Azure VM networking facts
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- show
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_vm_name }}"
|
||||
- --show-details
|
||||
- --query
|
||||
- "{publicIp:publicIps,privateIp:privateIps}"
|
||||
- -o
|
||||
- json
|
||||
register: azure_vm_network_json
|
||||
changed_when: false
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Set Azure VM connection facts
|
||||
ansible.builtin.set_fact:
|
||||
cloud_vm_public_ip: "{{ (azure_vm_network_json.stdout | from_json).publicIp | default('127.0.0.1') }}"
|
||||
cloud_vm_private_ip: "{{ (azure_vm_network_json.stdout | from_json).privateIp | default('127.0.0.1') }}"
|
||||
cloud_vm_admin_user: "{{ admin_username }}"
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Set Azure dry-run connection facts
|
||||
ansible.builtin.set_fact:
|
||||
cloud_vm_public_ip: "{{ cloud_vm_public_ip | default('198.51.100.10') }}"
|
||||
cloud_vm_private_ip: "{{ cloud_vm_private_ip | default('10.42.1.10') }}"
|
||||
cloud_vm_admin_user: "{{ admin_username }}"
|
||||
when: ansible_check_mode
|
||||
|
||||
- name: Prepare Azure Windows VM for initial WinRM bootstrap
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- run-command
|
||||
- invoke
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_vm_name }}"
|
||||
- --command-id
|
||||
- RunPowerShellScript
|
||||
- --scripts
|
||||
- |
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
$profiles = Get-NetConnectionProfile
|
||||
foreach ($profile in $profiles) {
|
||||
if ($profile.NetworkCategory -ne 'Private') {
|
||||
Set-NetConnectionProfile -InterfaceIndex $profile.InterfaceIndex -NetworkCategory Private
|
||||
}
|
||||
}
|
||||
winrm quickconfig -quiet
|
||||
Enable-PSRemoting -Force
|
||||
Set-Service -Name WinRM -StartupType Automatic
|
||||
Start-Service -Name WinRM
|
||||
Set-Item -Path WSMan:\localhost\Service\Auth\Basic -Value $true
|
||||
Set-Item -Path WSMan:\localhost\Service\AllowUnencrypted -Value $true
|
||||
netsh advfirewall firewall add rule name="Allow WinRM 5985" dir=in action=allow protocol=TCP localport=5985 | Out-Null
|
||||
Get-Service -Name WinRM | Select-Object Status, StartType, Name
|
||||
register: azure_windows_winrm_prep
|
||||
changed_when: true
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- os_family == "windows"
|
||||
|
||||
- name: Show Azure Windows WinRM prep output
|
||||
ansible.builtin.debug:
|
||||
var: azure_windows_winrm_prep.stdout
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- os_family == "windows"
|
||||
|
||||
- name: Wait for Azure Windows WinRM endpoint
|
||||
ansible.builtin.wait_for:
|
||||
host: "{{ cloud_vm_public_ip }}"
|
||||
port: 5985
|
||||
delay: 10
|
||||
timeout: 300
|
||||
state: started
|
||||
when:
|
||||
- not ansible_check_mode
|
||||
- os_family == "windows"
|
||||
96
roles/azure_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
96
roles/azure_dev_desktop_lifecycle/tasks/destroy.yml
Normal file
@ -0,0 +1,96 @@
|
||||
- name: Preview Azure destroy/cleanup request
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "azure_resource_group={{ azure_resource_group | default('n/a') }}"
|
||||
- "azure_vm_name={{ azure_vm_name | default(profile_name | default('n/a')) }}"
|
||||
- "azure_cleanup_mode={{ azure_cleanup_mode | default(false) }}"
|
||||
- "cloud_vm_destroy_mode={{ cloud_vm_destroy_mode | default('destroy') }}"
|
||||
|
||||
- name: Build Azure cleanup query
|
||||
ansible.builtin.set_fact:
|
||||
azure_cleanup_query: "[?tags.toolkit_scope=='cloud-dev-desktop' && tags.managed_by=='ansible'].[resourceGroup,name]"
|
||||
when: azure_cleanup_mode | default(false)
|
||||
|
||||
- name: List Azure expired VMs
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- list
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --show-details
|
||||
- --query
|
||||
- "{{ azure_cleanup_query }}"
|
||||
- -o
|
||||
- json
|
||||
register: azure_expired_vm_list
|
||||
changed_when: false
|
||||
when:
|
||||
- azure_cleanup_mode | default(false)
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Park Azure VM in lowest-consumption mode
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- deallocate
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_vm_name }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- not azure_cleanup_mode | default(false)
|
||||
- (cloud_vm_destroy_mode | default('destroy')) == 'park'
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Delete Azure VM directly
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- delete
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ azure_resource_group }}"
|
||||
- --name
|
||||
- "{{ azure_vm_name }}"
|
||||
- --yes
|
||||
changed_when: true
|
||||
when:
|
||||
- not azure_cleanup_mode | default(false)
|
||||
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Delete Azure expired VMs
|
||||
ansible.builtin.command:
|
||||
argv:
|
||||
- az
|
||||
- vm
|
||||
- delete
|
||||
- --subscription
|
||||
- "{{ azure_subscription_id }}"
|
||||
- --resource-group
|
||||
- "{{ item[0] }}"
|
||||
- --name
|
||||
- "{{ item[1] }}"
|
||||
- --yes
|
||||
loop: "{{ (azure_expired_vm_list.stdout | default('[]')) | from_json }}"
|
||||
changed_when: true
|
||||
when:
|
||||
- azure_cleanup_mode | default(false)
|
||||
- not ansible_check_mode
|
||||
|
||||
- name: Remove Azure state file after destroy
|
||||
ansible.builtin.file:
|
||||
path: "{{ cloud_vm_state_file }}"
|
||||
state: absent
|
||||
when:
|
||||
- cloud_vm_state_file is defined
|
||||
- not azure_cleanup_mode | default(false)
|
||||
- (cloud_vm_destroy_mode | default('destroy')) == 'destroy'
|
||||
64
roles/azure_dev_desktop_lifecycle/tasks/facts.yml
Normal file
64
roles/azure_dev_desktop_lifecycle/tasks/facts.yml
Normal file
@ -0,0 +1,64 @@
|
||||
- name: Normalize Azure desktop resource names
|
||||
ansible.builtin.set_fact:
|
||||
azure_subscription_id: "{{ azure_subscription_id | default(lookup('env', 'AZURE_SUBSCRIPTION_ID')) }}"
|
||||
azure_resource_group: "{{ azure_resource_group | default('rg-devdesktop-' ~ cloud_vm_owner_slug) }}"
|
||||
azure_location: "{{ region | default(azure_location | default('japaneast')) }}"
|
||||
azure_network_security_group: "{{ azure_network_security_group | default('nsg-' ~ cloud_vm_profile_slug) }}"
|
||||
azure_virtual_network: "{{ azure_virtual_network | default('vnet-devdesktop-' ~ cloud_vm_owner_slug) }}"
|
||||
azure_subnet: "{{ azure_subnet | default('snet-devdesktop') }}"
|
||||
azure_public_ip_name: "{{ azure_public_ip_name | default('pip-' ~ cloud_vm_profile_slug) }}"
|
||||
azure_nic_name: "{{ azure_nic_name | default('nic-' ~ cloud_vm_profile_slug) }}"
|
||||
azure_vm_name: "{{ azure_vm_name | default('vm-' ~ cloud_vm_profile_slug) }}"
|
||||
azure_computer_name: >-
|
||||
{{
|
||||
azure_computer_name
|
||||
| default(
|
||||
('vm' ~ (cloud_vm_profile_slug | regex_replace('[^A-Za-z0-9]', '')))[:15]
|
||||
)
|
||||
}}
|
||||
|
||||
- name: Assert Azure subscription is available when requested
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- azure_subscription_id | length > 0
|
||||
fail_msg: "AZURE_SUBSCRIPTION_ID or azure_subscription_id is required for Azure operations."
|
||||
when: not ansible_check_mode
|
||||
|
||||
- name: Select Azure image defaults by OS family
|
||||
ansible.builtin.set_fact:
|
||||
azure_image_publisher: >-
|
||||
{{
|
||||
azure_image_publisher
|
||||
| default(
|
||||
{
|
||||
'windows': 'MicrosoftWindowsDesktop',
|
||||
'fedora-gnome': 'Fedora',
|
||||
'debian-kde': 'Debian'
|
||||
}[os_family]
|
||||
)
|
||||
}}
|
||||
azure_image_offer: >-
|
||||
{{
|
||||
image_offer
|
||||
| default(
|
||||
{
|
||||
'windows': 'windows-11',
|
||||
'fedora-gnome': 'fedora-x86_64',
|
||||
'debian-kde': 'debian-13'
|
||||
}[os_family]
|
||||
)
|
||||
}}
|
||||
azure_image_sku: >-
|
||||
{{
|
||||
image_sku
|
||||
| default(
|
||||
{
|
||||
'windows': 'win11-24h2-pro',
|
||||
'fedora-gnome': '43-gen2',
|
||||
'debian-kde': '13-gen2'
|
||||
}[os_family]
|
||||
)
|
||||
}}
|
||||
azure_image_version: "{{ image_version | default('latest') }}"
|
||||
azure_admin_password: "{{ azure_admin_password | default(lookup('env', 'AZURE_WINDOWS_ADMIN_PASSWORD')) }}"
|
||||
cloud_vm_platform: "azure"
|
||||
16
roles/azure_dev_desktop_lifecycle/tasks/main.yml
Normal file
16
roles/azure_dev_desktop_lifecycle/tasks/main.yml
Normal file
@ -0,0 +1,16 @@
|
||||
- name: Gather Azure lifecycle facts
|
||||
ansible.builtin.include_tasks: facts.yml
|
||||
|
||||
- name: Run Azure create flow
|
||||
ansible.builtin.include_tasks: create.yml
|
||||
when: cloud_lifecycle_action == "create"
|
||||
|
||||
- name: Run Azure destroy flow
|
||||
ansible.builtin.include_tasks: destroy.yml
|
||||
when: cloud_lifecycle_action == "destroy"
|
||||
|
||||
- name: Run Azure cleanup flow
|
||||
ansible.builtin.include_tasks: destroy.yml
|
||||
vars:
|
||||
azure_cleanup_mode: true
|
||||
when: cloud_lifecycle_action == "cleanup"
|
||||
15
roles/charts/nvidia_gpu_operator/defaults/main.yml
Normal file
15
roles/charts/nvidia_gpu_operator/defaults/main.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
gpu_operator_namespace: "gpu-operator"
|
||||
gpu_operator_release_name: "gpu-operator"
|
||||
gpu_operator_chart_version: "v24.3.0"
|
||||
|
||||
# Air-gapped / Private registry support
|
||||
gpu_operator_repository: "https://helm.ngc.nvidia.com/nvidia"
|
||||
image_pull_secrets: []
|
||||
|
||||
# Operator settings
|
||||
driver_enabled: true
|
||||
driver_version: "535.129.03"
|
||||
toolkit_enabled: true
|
||||
mig_strategy: "single" # none, single, mixed
|
||||
dcgm_exporter_enabled: true
|
||||
1
roles/charts/nvidia_gpu_operator/handlers/main.yml
Normal file
1
roles/charts/nvidia_gpu_operator/handlers/main.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
28
roles/charts/nvidia_gpu_operator/tasks/main.yml
Normal file
28
roles/charts/nvidia_gpu_operator/tasks/main.yml
Normal file
@ -0,0 +1,28 @@
|
||||
---
|
||||
- name: Create GPU Operator namespace
|
||||
kubernetes.core.k8s:
|
||||
api_version: v1
|
||||
kind: Namespace
|
||||
name: "{{ gpu_operator_namespace }}"
|
||||
state: present
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Add NVIDIA helm repo
|
||||
kubernetes.core.helm_repository:
|
||||
name: nvidia
|
||||
repo_url: "{{ gpu_operator_repository }}"
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Deploy GPU Operator
|
||||
kubernetes.core.helm:
|
||||
name: "{{ gpu_operator_release_name }}"
|
||||
chart_ref: nvidia/gpu-operator
|
||||
release_namespace: "{{ gpu_operator_namespace }}"
|
||||
version: "{{ gpu_operator_chart_version }}"
|
||||
values: "{{ lookup('template', 'values.yaml.j2') | from_yaml }}"
|
||||
wait: true
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Include validation tasks
|
||||
include_tasks: validate.yml
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
15
roles/charts/nvidia_gpu_operator/tasks/validate.yml
Normal file
15
roles/charts/nvidia_gpu_operator/tasks/validate.yml
Normal file
@ -0,0 +1,15 @@
|
||||
---
|
||||
- name: Wait for NVIDIA Device Plugin daemonset to be ready
|
||||
shell: |
|
||||
kubectl rollout status daemonset/nvidia-device-plugin-daemonset -n {{ gpu_operator_namespace }} --timeout=300s
|
||||
register: ds_status
|
||||
changed_when: false
|
||||
|
||||
- name: Validate GPU resources are allocatable
|
||||
shell: |
|
||||
kubectl get nodes -l nvidia.com/gpu.present=true -o jsonpath='{.items[*].status.allocatable}'
|
||||
register: gpu_allocatable
|
||||
until: "'nvidia.com/gpu' in gpu_allocatable.stdout"
|
||||
retries: 30
|
||||
delay: 20
|
||||
changed_when: false
|
||||
15
roles/charts/nvidia_gpu_operator/templates/values.yaml.j2
Normal file
15
roles/charts/nvidia_gpu_operator/templates/values.yaml.j2
Normal file
@ -0,0 +1,15 @@
|
||||
driver:
|
||||
enabled: {{ driver_enabled }}
|
||||
version: "{{ driver_version }}"
|
||||
toolkit:
|
||||
enabled: {{ toolkit_enabled }}
|
||||
devicePlugin:
|
||||
enabled: true
|
||||
mig:
|
||||
strategy: "{{ mig_strategy }}"
|
||||
dcgmExporter:
|
||||
enabled: {{ dcgm_exporter_enabled }}
|
||||
{% if image_pull_secrets | length > 0 %}
|
||||
imagePullSecrets:
|
||||
{{ image_pull_secrets | to_nice_yaml(indent=2) | indent(2, true) }}
|
||||
{% endif %}
|
||||
36
roles/charts/ray_cluster/defaults/main.yml
Normal file
36
roles/charts/ray_cluster/defaults/main.yml
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
ray_namespace: "ray-system"
|
||||
ray_cluster_name: "ray-cluster"
|
||||
ray_image: "rayproject/ray:2.9.0"
|
||||
ray_version: "2.9.0"
|
||||
|
||||
ray_dashboard_enabled: true
|
||||
|
||||
ray_head_resources:
|
||||
requests:
|
||||
cpu: "2"
|
||||
memory: "8Gi"
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: "16Gi"
|
||||
|
||||
ray_worker_groups:
|
||||
- groupName: gpu-workers
|
||||
replicas: 2
|
||||
minReplicas: 1
|
||||
maxReplicas: 4
|
||||
resources:
|
||||
requests:
|
||||
cpu: "4"
|
||||
memory: "32Gi"
|
||||
nvidia.com/gpu: "1"
|
||||
limits:
|
||||
cpu: "8"
|
||||
memory: "64Gi"
|
||||
nvidia.com/gpu: "1"
|
||||
nodeSelector:
|
||||
accelerator: "nvidia-h100"
|
||||
tolerations: []
|
||||
volumeMounts:
|
||||
- mountPath: /dev/shm
|
||||
name: dshm
|
||||
1
roles/charts/ray_cluster/handlers/main.yml
Normal file
1
roles/charts/ray_cluster/handlers/main.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
24
roles/charts/ray_cluster/tasks/main.yml
Normal file
24
roles/charts/ray_cluster/tasks/main.yml
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
- name: Create Ray namespace
|
||||
kubernetes.core.k8s:
|
||||
name: "{{ ray_namespace }}"
|
||||
api_version: v1
|
||||
kind: Namespace
|
||||
state: present
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Apply RayCluster CRD
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('template', 'raycluster.yaml.j2') | from_yaml }}"
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Wait for Ray head node to be ready
|
||||
shell: |
|
||||
kubectl get pod -n {{ ray_namespace }} -l ray.io/node-type=head -o jsonpath='{.items[0].status.phase}'
|
||||
register: head_status
|
||||
until: head_status.stdout == "Running"
|
||||
retries: 30
|
||||
delay: 10
|
||||
changed_when: false
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
53
roles/charts/ray_cluster/templates/raycluster.yaml.j2
Normal file
53
roles/charts/ray_cluster/templates/raycluster.yaml.j2
Normal file
@ -0,0 +1,53 @@
|
||||
apiVersion: ray.io/v1
|
||||
kind: RayCluster
|
||||
metadata:
|
||||
name: {{ ray_cluster_name }}
|
||||
namespace: {{ ray_namespace }}
|
||||
spec:
|
||||
rayVersion: '{{ ray_version }}'
|
||||
headGroupSpec:
|
||||
rayStartParams:
|
||||
dashboard-host: '0.0.0.0'
|
||||
{% if not ray_dashboard_enabled %}
|
||||
dashboard-enabled: 'false'
|
||||
{% endif %}
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: ray-head
|
||||
image: {{ ray_image }}
|
||||
resources:
|
||||
{{ ray_head_resources | to_nice_yaml(indent=4) | indent(12, true) }}
|
||||
workerGroupSpecs:
|
||||
{% for group in ray_worker_groups %}
|
||||
- groupName: {{ group.groupName }}
|
||||
replicas: {{ group.replicas }}
|
||||
minReplicas: {{ group.minReplicas }}
|
||||
maxReplicas: {{ group.maxReplicas }}
|
||||
rayStartParams: {}
|
||||
template:
|
||||
spec:
|
||||
{% if group.nodeSelector is defined %}
|
||||
nodeSelector:
|
||||
{{ group.nodeSelector | to_nice_yaml(indent=2) | indent(10, true) }}
|
||||
{% endif %}
|
||||
{% if group.tolerations is defined and group.tolerations | length > 0 %}
|
||||
tolerations:
|
||||
{{ group.tolerations | to_nice_yaml(indent=2) | indent(10, true) }}
|
||||
{% endif %}
|
||||
containers:
|
||||
- name: ray-worker
|
||||
image: {{ ray_image }}
|
||||
resources:
|
||||
{{ group.resources | to_nice_yaml(indent=4) | indent(12, true) }}
|
||||
{% if group.volumeMounts is defined and group.volumeMounts | length > 0 %}
|
||||
volumeMounts:
|
||||
{{ group.volumeMounts | to_nice_yaml(indent=2) | indent(10, true) }}
|
||||
{% endif %}
|
||||
{% if group.volumeMounts is defined and group.volumeMounts | selectattr('name', 'equalto', 'dshm') | list | length > 0 %}
|
||||
volumes:
|
||||
- name: dshm
|
||||
emptyDir:
|
||||
medium: Memory
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
1
roles/charts/ray_service/tasks/main.yml
Normal file
1
roles/charts/ray_service/tasks/main.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
1
roles/charts/vllm_runtime/tasks/main.yml
Normal file
1
roles/charts/vllm_runtime/tasks/main.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
31
roles/charts/vllm_service/defaults/main.yml
Normal file
31
roles/charts/vllm_service/defaults/main.yml
Normal file
@ -0,0 +1,31 @@
|
||||
---
|
||||
vllm_namespace: "vllm-system"
|
||||
vllm_service_name: "vllm-api"
|
||||
vllm_image: "vllm/vllm-openai:v0.4.2"
|
||||
vllm_model: "/models/Llama-3-70B-Instruct"
|
||||
|
||||
vllm_tensor_parallel_size: 2
|
||||
vllm_pipeline_parallel_size: 1
|
||||
vllm_gpu_memory_utilization: 0.90
|
||||
vllm_max_model_len: 4096
|
||||
vllm_max_num_seqs: 256
|
||||
|
||||
vllm_port: 8000
|
||||
vllm_service_type: "ClusterIP"
|
||||
vllm_ingress_enabled: false
|
||||
vllm_ingress_host: "vllm.example.com"
|
||||
|
||||
# Ray Integration
|
||||
ray_address: "ray://ray-cluster-head-svc.ray-system.svc.cluster.local:10001"
|
||||
|
||||
# Environment Variables
|
||||
nccl_socket_ifname: "eth0"
|
||||
gloo_socket_ifname: "eth0"
|
||||
nccl_ib_disable: "1"
|
||||
vllm_logging_level: "INFO"
|
||||
torch_distributed_init_timeout: "300"
|
||||
huggingface_token: ""
|
||||
|
||||
# Model Mount
|
||||
model_host_path: "/data/models"
|
||||
model_mount_path: "/models"
|
||||
1
roles/charts/vllm_service/handlers/main.yml
Normal file
1
roles/charts/vllm_service/handlers/main.yml
Normal file
@ -0,0 +1 @@
|
||||
---
|
||||
36
roles/charts/vllm_service/tasks/main.yml
Normal file
36
roles/charts/vllm_service/tasks/main.yml
Normal file
@ -0,0 +1,36 @@
|
||||
---
|
||||
- name: Create vLLM namespace
|
||||
kubernetes.core.k8s:
|
||||
name: "{{ vllm_namespace }}"
|
||||
api_version: v1
|
||||
kind: Namespace
|
||||
state: present
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Deploy vLLM Deployment
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('template', 'deployment.yaml.j2') | from_yaml }}"
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Deploy vLLM Service
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('template', 'service.yaml.j2') | from_yaml }}"
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
|
||||
- name: Deploy vLLM Ingress
|
||||
kubernetes.core.k8s:
|
||||
state: present
|
||||
definition: "{{ lookup('template', 'ingress.yaml.j2') | from_yaml }}"
|
||||
when: inventory_hostname == groups['masters'][0] and vllm_ingress_enabled
|
||||
|
||||
- name: Wait for vLLM API to be ready
|
||||
shell: |
|
||||
kubectl get deploy {{ vllm_service_name }} -n {{ vllm_namespace }} -o jsonpath='{.status.readyReplicas}'
|
||||
register: vllm_ready
|
||||
until: vllm_ready.stdout == "1"
|
||||
retries: 40
|
||||
delay: 15
|
||||
changed_when: false
|
||||
when: inventory_hostname == groups['masters'][0]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user