
Huawei Cloud Cce Env Assessment
- 74 installs
- 19 repo stars
- Updated July 31, 2026
- huaweicloud/huaweicloud-skills
Collect metrics and configs from Huawei Cloud CCE container environments and generate a cloud-native best-practices assessment report with improvement suggestions.
About
Automatically collects metrics and configurations from Huawei Cloud CCE containerized environments and produces dimension-scored assessment reports against cloud-native best practices. A developer uses it to evaluate a container environment's maturity and identify areas to improve.
- Dimension-scored metric tables and improvement suggestions
- Requires hcloud >= 7.2.2 with step-by-step confirmation
Huawei Cloud Cce Env Assessment by the numbers
- 74 all-time installs (skills.sh)
- +4 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #641 of 1,042 Cloud & Infrastructure skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/huaweicloud/huaweicloud-skills --skill huawei-cloud-cce-env-assessmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 74 |
|---|---|
| repo stars | ★ 19 |
| Last updated | July 31, 2026 |
| Repository | huaweicloud/huaweicloud-skills ↗ |
What it does
Collect metrics and configs from Huawei Cloud CCE container environments and generate a cloud-native best-practices assessment report with improvement suggestions.
Files
Huawei Cloud — CCE ENV Assessment Skill
Overview
Automatically collects evaluation metrics of the huawei cloud container environment, outputs metric scoring tables by dimension, and generates evaluation reports and improvement suggestions.
---
Prerequisites
Prerequisite check: Huawei Cloud CLI (hcloud) >= 7.2.2 required
Runhcloud versionto verify the version is >= 7.2.2, andhcloud configure listto confirm a profile exists.
If it is not installed or the version is too low, see references/koocli-installation-guide.md for the installation guide.
hcloud version
hcloud configure listPrerequisite check: Python >= 3.6 required
Run python --version to verify the version is >= 3.6.0If it is not installed or the version is too low, The skill execution is interrupted proactively, and the user is prompted to install Python 3.6 or later.
If the Python version is 3.6.x, run the following command to upgrade pip to the latest version pip3 install --user --upgrade 'pip<22'⚠️ Mandatory Execution Rules
Rule 1: Step-by-step Confirmation
After each step, you MUST:
1. Print the completion status of the current step 2. Show the output produced by that step 3. Every Skill run starts from Step 1 — do NOT skip Step 1 and jump into later steps 4. Strictly follow this Skill's rules and wait for user confirmation after every step 5. Strictly follow the described flow; do not perform extra operations. If anything errors, return the error to the user as-is — do not try to fix it yourself 6. All execution rules take precedence over efficiency
Do NOT chain multiple steps in a single run!
Rule 2: Fixed Directories
After each step, you MUST:
1. Save all intermediate files under the data/ directory
Rule 3: Permission Issues
1. When you hit a permission problem, try sudo. If that does not resolve it, error out and abort the Skill flow
Rule 4: Anti-skip Check
Before executing each step, verify that its prerequisites are satisfied:
| Step | Prerequisite |
|---|---|
| Step 2: Environment check | Step 1 is complete; Huawei Cloud AK/SK have been obtained |
| Step 3: Container environment collection | Step 2 is complete with no errors; data/ and artifacts/ are emptied |
| Step 4: Metric scoring | Step 3 is complete; data/cloud-native-collection.md has been generated |
| Step 5: Report generation | Step 4 is complete; artifacts/cloud-native-summary.xlsx has been generated |
Rule 5: Information Collection Method
1. For every piece of information collected, prefer the Huawei Cloud KooCLI tool (hcloud commands)
Rule 6: Status Tracking
📋 Cloud-Native Container Assessment — Status Board
├────────────────────────────────────────────┤
│ Step 1: Configuration [⏳ In progress]│
│ Step 2: Environment check [○ Pending] │
│ Step 3: Information collect [○ Pending] │
│ Step 4: Metric scoring [○ Pending] │
│ Step 5: Report generation [○ Pending] │
└────────────────────────────────────────────┘After each step, update the status board:
📋 Cloud-Native Container Assessment — Status Board
├────────────────────────────────────────────┤
│ Step 1: Configuration [✅ Done] │
│ Step 2: Environment check [✅ Done] │
│ Step 3: Information collect [✅ Done] │
│ Step 4: Metric scoring [✅ Done] │
│ Step 5: Report generation [⏳ In progress]│
└────────────────────────────────────────────┘🔐 Permission Boundary
The AI is allowed to:
- Follow the SKILL steps exactly
- Invoke only the scripts specified in the SKILL
- Return errors as-is when problems occur
The AI is forbidden to:
- Temporarily modify or bypass the workflow
- Switch to an alternative approach (e.g. fall back to Markdown when html fails)
- Modify any script or configuration without user consent
- Perform any operation not explicitly listed in the SKILL
Handling out-of-bounds operations
Any out-of-bounds operation must stop immediately and wait for user confirmation. Continuing is forbidden.
Core Workflows
Step 1: Configuration
Prompt the user for the environment configuration.
| Item | Description | Example |
|---|---|---|
| Huawei Cloud AK/SK | Access Key and Secret Key for Huawei Cloud API | AK/SK = HPUAN3EWCG... / 1Bt5sdDU.. |
| Region | Region where the container cluster lives | region = cn-north-4 |
| Cluster name | CCE cluster name (Step 1 only) | cce_name = dify-cce-cluster |
| Dockerfile source | Source code repository (containing the Dockerfile) | Dockerfile = https://github.com/langgenius/dify |
Once the user supplies the configuration, save the values into environment variables HWC_AK, HWC_SK, CCE_Region, CCE_NAME, Dockerfile_REPO_URL respectively.
Diamond Gate 1: Get user confirmation before Step 2.
Step 2: Environment Check
Using the configuration provided in Step 1, verify that the environment is reachable and that local runtime dependencies are present.
1. Use hcloud with AK/SK to access the Huawei Cloud CCE environment and confirm it is reachable, If it is not installed or the version is too low, see references/koocli-installation-guide.md for the installation guide. 2. Verify that the local Python environment is working correctly, Python version requirement is greater than 3.6, If the Python version is 3.6.x, run the following command to upgrade pip to the latest version pip3 install --user --upgrade 'pip<22' 3. Check whether the Python dependency library is installed. Go to the references directory. If the Python version is 3.6.x, run the command python3 -m pip install -r requirements.txt. If the Python version is later than 3.7, Execute commands using a Python virtual environment pip3 install -r requirements.txt. 3. Empty any historical files inside data/; if data/ does not exist, create it 4. Empty any historical files inside artifacts/; if artifacts/ does not exist, create it 5. Once the checks complete, return the result to the user and wait for confirmation on whether to install dependencies or to continue
Step 3: Container Environment Information Collection
For every metric listed in references/cloud-native-checklist.xlsx, collect the corresponding environment information.
1. Every collection run MUST be driven by the actual metric items in references/cloud-native-checklist.xlsx and MUST invoke scripts/collect_all.py to collect fresh data — do NOT reuse historical or cached data 2. Acceptance metric: the metric item being assessed 3. Quantified target: the reference standard for that metric 4. Acceptance method: how the environment information for that metric is collected 5. During collection, if any required piece of information cannot be obtained, explore alternative ways to retrieve it and ask the user for confirmation 6. Once collection is complete for every metric, fill the collection method (including, but not limited to, Python scripts and executed commands) and the collected data into templates/cloud-native-assessment-template.md, write the result to data/cloud-native-collection.md, and show it to the user
Step 4: Metric Scoring
Based on the collection data in data/cloud-native-collection.md, score each metric:
1. Compare the collected data against the quantified target to derive an acceptance verdict (Fully Satisfied / Mostly Satisfied / Partially Satisfied / Not Satisfied; metrics that cannot be compared are recorded as Not Satisfied; "Not Applicable" and "Not Evaluated" both roll up into Not Satisfied, and the basis field must state that the metric is N/A or Not Evaluated) and a scoring basis (describe based on the collected data — include the metric's environment information, the collection method used, and the reason for non-satisfaction) 2. Convert the verdict to a score: Fully Satisfied = 3, Mostly Satisfied = 2, Partially Satisfied = 1, Not Satisfied = 0 3. Invoke scripts/score_and_excel.py to produce a fresh scoring sheet, output as Excel containing the columns: number, cloud-native dimension, level, acceptance metric, quantified target, acceptance method, description, acceptance verdict, score, full score, scoring basis. Save to artifacts/cloud-native-summary.xlsx
Step 5: Report Generation
Using the contents of the scoring sheet artifacts/cloud-native-summary.xlsx, generate the final assessment report:
1. The final report uses templates/report_template.md as its base template 2. The report title is fixed as "Cloud-Native Assessment Report" 3. The report contains 4 chapters; Chapter 1 and Chapter 2 reuse the content of templates/report_template.md directly 4. Chapter 3 of the final report is populated from the scoring sheet artifacts/cloud-native-summary.xlsx 5. Invoke scripts/make_charts.py against the cloud-native-summary.xlsx produced in Step 4 to generate a radar chart and a staircase chart
Chart generation requirements:
- Radar chart: group metrics by cloud-native dimension. The score for each dimension = (sum of that dimension's metric scores / sum of that dimension's full scores) × 5. The radar has exactly six dimensions: Service-orientation, Security, Automation, Elasticity, Observability, Resilience. If a metric belongs to multiple dimensions (e.g. metric #1 belongs to both Service-orientation and Automation), it contributes to the scoring of every dimension it belongs to.
- Staircase chart: overall score = (sum of all metric scores / sum of all full scores) × 5. Stage thresholds — score ≤ 1: Traditional; 1 < score ≤ 2: Basic Cloud; 2 < score ≤ 3: Service-Oriented; 3 < score ≤ 4: Automated; score > 4: Intelligent. The chart must visually render as stairs.
6. Place the generated radar chart into report section 3.3.3 and the staircase chart into 3.3.4 7. Replace Chapter 4 of the final report with the actual remediation recommendations 8. Based on the assessment results, output remediation recommendations by P0/P1/P2 priority:
- P0: Not Satisfied (0 pts) — mandatory items
- P1: Partially Satisfied (1 pt) — items to fix
- P2: Mostly Satisfied (2 pts) — recommended improvements
9. Invoke scripts/make_report_html.py to generate the final report as a html, written to artifacts/cloud-native-report.html
Core Commands
Get cluster ID and basic info
hcloud cce ListClusters --cli-region=cn-north-4Get cluster kubeconfig certificate
hcloud cce CreateKubernetesClusterCert \
--cli-region=cn-north-4 \
--cluster_id=a5659ec8-55b5........ \
--duration=1Get detailed cluster info
hcloud cce ListClusters --cli-region=cn-north-4Authentication parameters
| Auth mode | Required | Optional |
|---|---|---|
| AKSK | --cli-access-key, --cli-secret-key | --cli-security-token |
| Profile | --cli-profile | --cli-mode, --cli-region |
Output Format
1. The final report is delivered as a html file 2. The scoring sheet is delivered as an Excel file 3. All output files are saved under the artifacts/ directory
Verification
Post-install verification
1. Version check: hcloud version MUST return 7.2.2 or higher 2. Help check: hcloud --help MUST list the available services
Authentication verification
1. Profile check: hcloud configure list MUST show the configured profile
Best Practices
1. Evaluate and analyze the container environment running on Huawei Cloud. 2. Harden the configuration of existing container clusters and modify the cluster configuration based on the optimization suggestions.
References
report_template.md— Final report templatecloud-native-checklist.xlsx— Description of assessment metricsrequirements.txt— Python dependency library
Scripts
collect_all.py— Main assessment script; integrates all collection modulesmake_charts.py— Chart generation scriptmake_report.py— Report generation scriptscore_and_excel.py— Scoring sheet generation script
Templates
report_template.md— Final report templatecloud-native-assessment-template.md— Data collection template
Notes
1. Make sure the Huawei Cloud container environment is reachable 2. Make sure the Huawei Cloud credentials are valid and not expired 3. If the Huawei Cloud Koocli tool and Python environment cannot be automatically installed using the skill, it is recommended that users install them manually. 4. Currently only public-network access to the container environment is supported 5. Output may be truncated during execution; clearing historical data and re-running the Skill is recommended in that case
PK
�N�@ docProps/PK�N�@��j�dddocProps/app.xml���J1�����來��L/��vQݧ�3m`& IZW.w��R(.w�J��6m�c����+w��?��/���Q����R���1�d�� �G���F�R�D
�n��+~WK�r0�Yࡵ�E�aCH��,�K�R�Z= 2�9�]�NR�4��M#"����q����M#�r>s�+��J%�Q�^r����E{# ڑ�rJ?DP�Q��(Ak^���I��4_L�rmB?����~�V�ĨO
�Ψ�TX���MQ'�XΞ��7��{�8�<+��h���a�p��`nPr8a���m�w��7��C�[�L_�f�������|��8������-��D���~PK�N�@/6�=GfdocProps/core.xml��_O�0��M���P`�i,�ɞ\b"F�[��m��4m���-lC}�=��z�M��^��+U�$�Q�5\�m�^�Ux���n�����E�4a��'�h0N�
<IY�t�v�i��e;��Fޡ��i����k�>�p�s,�QN�=0�#���H�i���$(gq%����H����L�R�N�NǸS6gqt��m�F�l���'�m��<T
��w��9g���1e�_w�:Ǔ�~�5�n�w����K��u����o��d��OEN�����Z�2��yga�UIBf7$���������yL�⢊�*#i6!����g��PK�N�@6�v�*docProps/custom.xml��]K�0���C�}�4]�f��XA�PPw+%M�B��$��1�x�^���_�\�A�>��K��A�2���x ƶ�kG%y���u~}��k5qmn�CH�����
!��\��wk�6�Ң�n�;��~`�Vl\ZD0���U�p��[�_��b';�}<NN7O?�G�;t|����#ydC+/�A�ѐ.=�`LJR5�ؼC0���
W���:������X�Ӣ�qX�aR,Q'8^bJ�e��: �瀤�;����?���Ý���̖�0v[��/ �{�O�h�
:]����PK
�N�@xl/PK
�N�@xl/worksheets/PK�N�@+a*{&
BCxl/worksheets/sheet1.xml���n�H���w0|?�e;�F�#[��v��=\��cLgmwgf�~Y"%��ʠ�N���U�Y����=��ׇ�v�r?N.&�Q���?l_�ޏ��������xZ�<���/�����8���_���m���Tקyx9ޏ�N������S�[/����<�����<|�<���C�i�|9�L�/w��˘=|8������^�7�v�ˉ������|ھ[o�=�)��������[:�<��n�9�����f���ū���3��m"G��������+-���y{��Yn;����y{{�x{=^l^d
PrsY�ߎ��n�>�ǟ>6;�����LJ-Q[?:ԏ�㟒�|>��F�m�vT��N�/�����T?P��G���_6�g���+��?¦?�1�ɗ�����3u��^�/���_^iM�����o��iQ?S��fw��zs�~�&�����Ds�&Odz<���_��6��I�����#;����<��Yr���^vֿ��ϛ ��0z��ߞO�������1MUl�ܿ����SXQ�p�����h��Ԛ�G��o��7�;�����t����%q��yu� oӝ~J����]�yr7��Z��O�>�^���'����o�;�lG����_����AB�����mDiIsm8]]�r��Yt|$��Bc�"A0�GD�H��&/���lD�����S$V��S�X�b�|̬"�s��cŕU���*JVP�tk���Ϭ X���*V���*�X��P/ig��!�a{f���9!y���Ͷ���(L��& Y��.+�7��;����n���Ƒ���I���ِL:�*�T�b�R�
���-@I�͠�0Y�d��B)s$Ky,���Ԥp�H J�(�l�h#K�--�O@;�>w�����r� \��u� �X2��� L�p$�%K\6h���������.Y�����SI�m.K�:^n,�B��2�p$�h%K\6h�����.��u85��;�����Ѳd���ƒ)HrG�h -%KZ6h����������-�5�A�,D�xnY,��a�ǒ)L��%3<oY�вA��,�������\H����w�ɂ%�/��r�xA��d^�X��%38�K�(�l�p#K�-.���%A֜r� \�����:^`�,�����cI7����+Y��AÍ,���ԭ�{�7�wp�|J�q��h�z~����A��&"�hf�n�hc�ЏP��{�Ul���b��!q2
�΅�
���*F��Riՠgh!�a�<����9c��v41�x�9��ݜ[���b@�#e��ʂ���U���@s="%L(M��@��Xx8G��Ѡ��<
�����c��4xh�FG4�TQ]�H�*c�������'\�iX}*�äكр���1��\�<
�j�i �
O�S�F��e������,h:1����\�b�k!
1��#�p�hBW����v�@�d�X�c���9�����<C�+)S��Y҄�!}�����In�� �&>Kј��"��2�(�Mѭ�eXv3l-�Ţ�bSeL+�l�Nn�0-�� ���&�h|Hͥ��!�4乧�M,<
y)}T�J
��TI?VY��(�>��'!Ew?�@X�ҪI�!�4�8�Y��\C�d�Xװ9����&�34�#S%�<Ҵ�wH��'
3Jn5A��4gK�Ґ䙧����
�F�i 7J�hҼRC:2U��#M��{��7r|��ӄ[5������,=?�ҙ����=
~�i �K�hԺ̓;��TI?�T��sO�B{�B��j����h��C4f� 52O��{�s�hn!�K�(�b�!�*c2���*���#(:��zB*���-$�B4z7b�q�vQ�ycA�����4�e7���C,�t\�%-����T׃S��J�&ǴS3ť���9�4�����/<
�})�q=("�2&K�J�����)=|J�+=��j�w����4�b���f�X�c����/<
�}����[�Ȑ�*K���@�.(�*�y�Ծ�4�9��j�D����W�w�ϙ7D}�i �OQ_�FGt\
��p�*˙j�w"��'�h��t��V�����`�0����s挕L`[sW�Q�"H�RD6OR�]�H�*c����y�ȅ���PK�ܪAưcM|Id�N�d9��`�)$�!�)'���碲�`ޅ+�)���gOS�]���QW�dqӬ��M��f�N���� ļY��$�-���P����Y���T)\>q/E�#�[r��֕F�U9q���\S{�Sn�,cގV�?4�7�-)sEp�2�pE�(��4k^� ��TI?VY�T���H���tʭ�$�BDfC"�R|��Ň.�;��`�+�I����0�,C[f�#[�화��o����m�Q*�üE4�[4�E�>��zʼ���?wE��+�(E��[,�wl�����N��/&6/�w���TZ
�)��BDwtv�� O2��7Ԟ��$w]A�r�����vR*����p:Lf��7��$�VK2`!�a�<�q�{�y��rWŶ7\�,Ұ�bb;2U2��Q����Ƞ�qôSi5��nv1��5�Q��A*�ޔ�]�+�,)E�q���TI?7U?�����_*�>�[
�7��q;��}�7�"wE�'�w9�"Ҹy�wd�������wps%�!��Y\
&1ni�W����ߝ��"H���9Q�"X\)"��WbpG�J�y��Jz�0�:Jpõ+�9�`��Ew|�t�a��mG@�f���'��մ��ͳ4�#S%�<�t���zg$�oX\*��.�OA��9��@)�D�ƈ��3Ws*E�y3�;2U���M��s��{ذ�����],l)��+82W'@� M
W�T�H��ؑ��~l*��<�r������!jSi%��68�m�1�[�0�����ç+�ݬϪ`V���D)\�s��F.3��W���T���K]y.�C{Of�J�;�E��G�֓ay
�@� Yr_�R�*H�RT�xۯǻ�M�1Y�RY�{iaΥ_.�~)�VS~G�D4\*6��.��� �U�.���t)��w�N�~xTnVFe��߹d6�m�y�GK���(�a(S�����^r
Y��㙭�w�����
}F�@��/ȘRT:�e�zd�Z��B�
i�9�
J��J�{���0q��,%8�v<��� �U�2����)E���L5��T���T$5a~�S��Q��,��TZ�c�>��K�fi�q���Y����)|�*E���L5���� p#2t&q�����C����ׇ�ۗ�虾5��s��J�:���� ��@��}9EM�r��wl>��������������PK
�N�@ xl/theme/PK�N�@��
�n=xl/theme/theme1.xml�YOo�6��w tom'�u�ر��M�n�i��XS�@�I}��úa���0l+��t�&[����HJ��K��ՇD"|���#u�ڃ��C"$�q۫]�z��>�8h{w��K�
�c�xL�ޜH�����]ś*$A�>����J%����a��<!1�M����WT�݈U֪�f%�4�P�# {{2�>ACM��ʈ���J����&M�;��4B�e� t�Y�>c~4$���
&�^������
�L1�bma]���u��t���(gZ��[Wvr���2���u{���`�M�,E���F���,���2�n�Q�����%�[�N��Je�D
�>֗��f}{����7����v��t�d��%|�J�Yw�2O��ڡ�~J=�L8�-�o|���(��<�4� �ժX��}.��@����'d�}��.�F�b�o\��C�\Ҽ��MT��0��z�����S�����ώ�t�����--g�.����~����?�~����xY����'���y92h!ы/����ɋ�>����%�m�GE��FD�[��t3�q%'#q��Sg�v �
�9fe�q�wW@�(^��wd�b�h �a��8g.J
pC�*Xx8��r�bV�`|Xƻ�cǵ�YU3J��ݐ8b�3+��(�������Q��u���K>Q�ELKM2�#'��vi~����vl�wu8+�z��HH�J���:�)���
~��L��\�E\O*�t@G�1��l�m��~C�*u��G.R(:-�ys^D��i7�QR��8,b?�SQ���*��q7C�;��+�}��ݧ�;4pDZ��� �K(�N��h�wŘQ��6�㶷
[SYJ�(��p��»�g�>�X_�x���wu�{���\>k�]X���y�}�钣�M�26PsFnJ�'K�,�}����䇦$�Ǵ�;�@`� �>�*�8���i"�LI%\������x�ӕ=6��������u=�
r2f� ��3c�� ������(��:�jZ�3s��L�s��*��U��ܚЅ �]��M8�k�p6���������x�"]$C<&�����>�'e�b. vJ|��y�X����ɾ��8�Ȯ��]�7�R�/�=��,.&'��Q�k5��q��&p���(�K��a�ݐ��
�S��d��L17 jpSa����S!���
3���5'+�Z�zQ
�H
)�7 �5)���k�dB|UtvaD�����p|�Fl&0�_�*�3�n'LE�/p���m���&]����8fI��r�S4�d7y��`�
�n���ί�I�R���3U�~��c�nrF:_�*�P����}���-p�Tp�l�r��ۜ�4LZéO�
��
!�P�L��B���]�$K ��*�++��6�5���v�ꦚ�e��NƟ��f�(�MN1ߜ��6����&3(��a��d��E,�U�z�<�{���E�Uϲ���V���)�9�Z[��4^kd�5���!J���?��Q�3�qBo�C~���M����m<�.�vp�����IYӦ���Z�Y_p���=al-�Y�}Nc�͙���ŋ4vja��vl����'S�&�A�8�|�*~x������1%-m��PK�N�@<{T�xl/sharedStrings.xml�][S[W�~���*O��Luۀ=�Ng�<��M�I�8���c%�`�H�Ô�\$n�~����cBB�_���=�/̷��� �=��)�����k��^�s�7_�n���Cm���.�]����w�n�u|�օO>~��|�psǭ��@�����C~S���p-
�з#�օ�p��͚�PK��vs�R��߁7��������&��7�
������5�kkYs���ク%��~���_b�������7ȓ�7j/�_��_�ۇS*��&\�����|fL�.��vvWex_ۙM}�}Zx4lM�Y���@ٻ����瓕�I��u����q�x��Tf����҂��:�V�~�S+�Z�������q�d��G��Z� � ��w���w�BM���@{ �~q��S�����waigo���1�mm���qݕ�g5<f��ZQ���LJ�Us>¥7C��-�1 K���P���TW��g��zg���roln�IO)���*y�f���ߞ��G�p����g*3aOl��M�ޡ,>f�{o�|��[v�;��5Y�XV,b-���9O>�!#&�ݸ=�k1����J�U{�q�YV����Oo�M\z�'������JD��H>�T%���g���Z�T;��c�Wkk���0�R�̨Z�ثi�����Tf�V�?�*�gg�����P}�52@>;�2��ݜ��vR�ݝ�n��7S?tW
����s4��H��O*�t.�a��Y�g7���oygz;��?�N[���玟�Z��[K�*��GSj���7,'�F��Y��٨�F�����Y}��}4���g6��C�ܨ������_"eh�O�1���a�jd\Ҏ�x�=}�������c�7��W��]kjG
��8*u���=3�E�G�դJ�Y�M,�0�'U��R��E�ٔ�Rɘ��*�-�>�N.>kz����3�18�=F�~1�r�!�hb�����5��
�I���y+���ڵ1�
�V���O>���g�Ax��iݤ�"4��` >��b���g����C��`&�P<W�3�BFE��
a�Xa`D��H<�� ��d+�C}D��������4l4���iB����tÇM����N�;;w��]z�O�b����Q�2Ahv\����-�4k���m}��),��"#s�<��:�7
�����%i�}}K@�^�DB�n����P��b9��p��@[G��<5������g��o� ,B�*��l�6p4*��"��<����LS�۾:Z\��̴�5�
�@�����H>�`?͂��W���|�s��w��#p�m=�d���x
�}`XX�=EW
���\�ܬ5���J�ьLP�4��O�ʤ'�g3�1p7���p(�@��,�?�![��F;v@�2`oy��Y}v�Y�4�tR
>���vv@��;���t�i=�fu���QZ9.-�TUWOm��c��t]jTskph"�3U�n�����hv���
At�&�'��:��~E3���N�J`���3�Rp=`Yaqż�5����AHvr�ʱb��*8n7��"�$����5�yn����#�ίf�U����vO�w��1&0Z�#�X,�3^��}�H_a�ZV<��"A4�>eg����Zφ��$8+d<�0�TQ/��ad��e#N4J<���$��<��ņ]`�j��*
�"U���fa���Y��5T�G��p]쑥�Z�{�����J��ǤO�ڝf�Έ��H%Ƭ�C�d�T[�9�γ^+� %�81�a���Z/$<'M�?�
��4�wz�%(9��>)���xʭ�掠�:�9@�������-�PR���]ёT��1gĖ,��g�葆l���{�o�5�d��Q�}N�f�tp"t�`Q�r���&��^���{Ds�}�����tbZ6��P�|�(�TX���`��w���
��vL�ۅĕ�M�2mt�oR��Kk��Ǝ�t�o^��Xz�����D�v�����W��=;�;�wZ�[#�zZY���D�x����'>Hj2�a�ؤD8�=vρZ��n��()��/�*���5�-8(+�,tt��P�H�*������Z
˛�]�
�w��4��ӲV�ܤ�E��H?.Dz�ǻ����H�{�M�2��(��}V�A��[�ű�gUO��{�\��u'-㍮�_ke����(���_�5Eí\`g'�44�-'y��0}ImQ�@� ��E���n�m��������I��v��?�����:>��~�?���������_�<�G��"Y��|~u�gs���=�!��>h4WkI��%\���x���گ��x�\�Q�˯��Cr��r��&P$����wք�#��D�.�����4�\�&���@��)�E�:��j��0�.J�Z���2Y�5Hzp�?|��^[{�?h-FD�Â"��AR���b% � cM�XO'�!�����͔Nt8���le��|�r�̟�#�m?մ��n��z|�˴p|�UKCt�a�#���
GB���TU�I�e<ߪqjrKqv��\�
'�o��矴���:��ς����}�OW@�{��ނ�xN�J��&[�f��A`�!����6��ia")b�@]���uo^� �H����qjd�?^���
�b�t��]�`3�B����\a�ZbA���.D�/k�mT|Z�E�
Z���2�Z�_!c��Op�v�>�EX�t}��I8dzbN�Ӂ�[����J2��;>d00����2yYUV�#p鉥
��f�Oy;`�e����a�x7<�%���lrA_dlC�����-m�)�l�N���,�
��WEw4T��O$���g�����fP�o�{�ĭ�����:�h���,y=��0�Ci��R�Iu�C�cQ�#��Ƶ^:�U��P���_�R��H���o��ݲ�,��?���9jz�d�l}Wj}��=P��u����O��)�aaH�-�-1���'�}姀�ե��Ɔ��*Nv_<:�증Q����)�%SG��/N,H�kd��'�-?d�L��v?~Ϲ5��ҋE�t��n��a�;�o
3�b�&�C�E���M��OO�r��=���®:�F-���T�1B-������W��d��nX{v�Bʳ<q�]�@Rkn���#g��W���ƻ�]�m�T�����^Qz��b�I��A��\�fXwIX���fFq�G����E�3� �;˘�˗D�K���������`�1T
�0:��'�+�|dA��-����)(>�ɍ�!�Oy��Ma""-
"��H�3��
OhqOfT�O'��[��p\b�"d~Bwh�8qu�:!���:)���w�BF7�dA."|Gn��I�.�3��O��5v�S'��cqЎ�gr�
s� ��D�0p�D8f�ȑe�9�Zٱ|z�JG)� ��L�&&f�T
��L��j1^I���P�}�JQkr1���˔Ĝg���EM<���Ti˗@�u�dB��#�sY�+ďDt�h�?��4�G��E{����C�|5����pk%B�v�?���w�C!��������Ƅ�C�{]7����2���eQCUt�P���ne�u:�>V��?A>�\ ��k�'Ʒ�[�t
<�:Ye {�̣�����-]�
sq
����YJ�E�>{a��Mr�W�E.���z{���;a�M��S��|V��&�挸�#.V��Lš��
.'��T���Gq0�ppx枓,I�>��7�au���c�ڻ<�L�Ɲ�8�@���5ld0�iU1l���|b�v�R��$�}H�Q�DC�Y�ka:Y���"����!��B���t���0#���Bu&����A!�r�Ȩ�������,dH����}l�2L|��Kgw�#�&{�K����r"ع9ͪB���R1���Xrf9U]��'�˗|�ϗݳE����N� h价�V���M'�'Z�Oj� ���|�
�N�U�"��:!M�q ��z �k�,W�(ip�PƱ�c0����D�*(�r��4�nyo�"+��N��|�%�Hথ��s���kQ�e.�fGݞ-��S��)����8���*dp 1�0.cњq�[��t)��g�8봍C"=qϤ[�(�ť�����k��X�[�i
���Vy);A
)�J��$�y��m�.�ޔ܁��m%q��w�2e�;���@�, ~�=�u�q��ͼ�a�+�{�
���G?̌"DG��;i�Pw?$�B��y��q�G��u5��SL��O'�'$�P�Q,Ab�e]�'&�އ�:�
�HFl%Pb����]�}p����b=��HH��֖�O������)3J��ȥr�O�?�
E���_��8�!d5j�/���b�PF��bgKĔj8c�W)m"Nl��e�E�ǺX���O�G��������f
�[�1;��ey\:nU��U���S?�U\���hw��!��&��p��V�t$�ˀGGJOS�kq@
�;W�����SI��){rCh�g���Dkq`��<y���J��+IM���ڏIܑαq�K�Fǫ�,�ޘN�F\�#w��1��fX�f
E�[FJ�����剜�S��xJ���r��C.¥�a�ڞ��<�M���Q�!��@sa�� ��Y]��0,��@&��Q{�ؒ�c$|RL�<)�*Dd��Tt��lgIUɇt�L!�럑�D��j��L�wlTZ��YƖ��4C��?3�:�wF��4��6�@�����,�4�L���"��S:/�o���)�`/��zy�(�~� ���d��p��n�(?c��Ö-h��i*�^����&�� �)7�.IV��vK8�.I?��y[G[�Yl�X���~��c���c�zkȈ{�C4��2�.kP��k'D�8��MSڷk?^�gb��A��=u��1��f���p�*V�݀�prk.��v���Z�w9��`������^n�C��d(o_}L,�\I`B���U��1����SGC��aZwM��QF��$��v��v��?��1��E� }���`��-�T2�������0p1����̚9rƦ)�������BY�����"����4�wO���Z�/���p����0Q��kB��3�,�Sh8��r�3����DKv��{*8ᦑҕUW�QL��K��뇾]��߸�8IU��=?f�n"����]��9�BJiY��Zz�E��R��v[G ��#��n.+=���o>��������ʋ�5��igz��������q&#����ot���$��fhF9u���x�~� �*�� Rٚ����`CU�ލc��4 ���8�Ӟ���Op�Zq��%�S�3��<ȍ���~��d���D��)Z��I��J,��Of��O�7�U�z՜��TL��(_5���`[K����&�v�9 x���5��wn�<a�A{�-G�
��%
!�\�&
����A��?�n�.fn�Wv�k���L� P7m�rv���
^�XlN�vr�Bǀ����,�SPW�7�L�h,�D��N@�q�kwI��eX�����槡8(�F>7�cam���Y��-5$Jt�2���)�nJ<Hr�w�������Y9�mUz��
�
t�k��?,���K����d�P���T|G���Q�
���겞Cig�r��]r>�s�$���|�@V�kHO�TL″y����r7�d��1�yE��x��D3���P��P���Lk*G����h ʤ�S�8�L�˓��r�f�@¢zUlT�ï���=�Ԯ$���?�\<
��� �C(���$��*Cj"��q)#ؓ��b�Fc�L� t�Dz~�����C<<P�q�X�ˁN�x
��*DEL_ۭ_@Jv�_J�/j,TB�T��E� }81�<W� A젏��r�{�߸����ThH��!$Y�#ɪ��"^�|Р��2���#���[1{���LJ%P"\#~~ï�x�3�J���
]D=ߖ���1���/|
� K^�iCK������*�(B�gt����1�ؠ�����JT;y�'d���=V�d��VXXsH�LM����0��ҽZ��6���`Xx��62�
�H��-�ȼ��ۢ��:]r�FD,��=H�v��^����!4s �Ǎ�3?�@��z�E�P\5M_P8��4���d��\���s �(��.��H5AAD��sYF�\EZ7:Aii-صbqIȖ������릸4X�'3P�i��x!��H���r8� ���D'Vl���L�<�ŔC�cŐP�+�S��L�N��q!E�-[n��K�%��yYjK����Oh��V&���2E/N�c��y!.tӉ;6|R��k��Vڡ�tU/�X.i6R�M@�eO�ЫE�t+�Һ� ��G]�C��7����v:.���R���|y�G�ٜ<��C�NW�w1d��.��=R �b-V�025�b��PU!���,�K���4q�Sb�����xk����} J�G�Z�"��Q�7�S�b���[����c�\��BNC�Ld�@VJ����8�.<)�5�-o��8�$ۛ҉RU�<A#�T4܌�O���%�/eP"R�!�4b��@t����g�a�)}YUD�K�8��܀T|U(�CP�]�!R|��W�*���+
ܐe�����)�6���.��m��ق؈�Y8�
���ר�Z�`�k�z���+bl���by��-�
Luم�
��Cn#!���#M:'%��I���q�Z˩Ĉ�0c*zЊ����Ț�d��@W��_%�q]@��7�8l�bG�����Ts*
�{����[��:�\��g�ԫ��
ɡ���p���\�e.5�5����5��b�)���o���A����5�<������Ae�nU�E�kN}���rk�q�D��=��o�DY* >���O��W�G�b��kg�%��i���p�_�aU���T�\^��㹒��vU�o�=槰u��vE9��
�^kWl�|Ah�֮^�~T�
�K�.츨��:V�Ξ$��HZ VM� Myg���ſ��F�,(.�)-����j!@��j~̀��㘾��ϷvuR��K�]��n]j ܮ���k��z����f�ᶎ�--��������r�R+>�J�Xrz����&�S�,W���Ze��\ۥg�8��pd� �|�O�Q�>���
x_�D�X5�k�@�Q��u�<kt�I���3K�%�2F!���R�����H��[:����8��T$�'�|�V�ar��Jl�_nRm
�����g|V�0�8�V$����"�
evS��'� �?�)���E�g�tJ�' /ΉL�gT�R�Â(1u����L�<Vˈ�S�t��1�nY�ix�ż4��XOXbZ����?�7���%�����HJ�H� L��}��_R��*�擨q:d��#)e�FΕ��N���f(TP���8i��}��)�����@��%��''} u�[��\��7�F��K���`,q/S$�n���I��1D���zq!ǧeH$Ap8%P�Q]���i�Y,�p"�9q���W"n�%%�͵��!Z��e�z�c/�*��Ǝ/�TP@��n�*U�9Y�B�t�~����������w5P���z�i�C��+�F�K}���qӿ��W�
� Hv����\Q)d؏:���lNo�O���'�ј�&�ϧ�M�D'��h�}y�����x½�(�Z��p���Ը�%u��Ԅ�Η�vC�t�AM(��?PK�N�@wYђxl/workbook.xml�SM��0�#�,�['�XҪ��6b��jUJN�M&����.)BH��!!qߟ\���,����@(��<�y����wi���TLp�m#��߸���o9)MyH���/@����{�\ȫ�W�p��X�lH�
bH�j��9��L�6���I���t���e��2�+��l�!��0�6�+ զ}�L�jẼ蠙ú�g�pE]�&{
�"�����,;��y�.�(�J�B�!tqפ"�;����fޖ%�tе:���\H�����/R�3����:6�;G�q�����Ŏӳ
9�D� #UF��&o������LJ���|�t�����fb�ɧ�-��8d�C��v)X�4 .$*BI�VgP0`�ϔ.#�J��^������������yG�V�w���ɬ↑������zRH�D�ہHI5ƿ6�vHY
To�Y��R��G`T{'��`��O�W����,z
���!��|��7�͖�.����ܛN��'����r������Pbfnv��<����/PK�N�@��p;<�Z
xl/styles.xml�\mo���>`��`�}fK��7�r�&P�
$lC��&F�.Eev����k�(�-[W`]�,���[7�A�$&��O�;���K^�t*�tD�s�s�y�^r���Ȕn��İ�m��^�%}ܷ�x�-���֔�����5���>��n}�k���ԯ��#���-�;��F�4����d�:����вG]��{�Ɂ�w"42KJ�\/���Xv5l��i����O�k}kt�u��a��%K����{c���L�zh�|�pQ=2��5���:�*Yá��#+����2HtZ���x:�F�D�[ӱӖ�������NVd�u�c
�M�[ҕo_�R�)�F����}�弶�~�+�sS�K�)�W �u����{��D~�V#?�'R���AxV��!�� N�ի��T��#`i�|�_=?c_��ewksh�Y��:d���ڜ�-���L*$C}˴l��C��qw��W�N~���}z�~מ@#q�*9G��w���%�FȒ�,�{���ie���^���;�����wO����~g^Esy��.P�OΞ}z���_���������Oq�P�5��J��r6�jM�jY�E�s��L�z8�M��k��T�*i����E�kh�/�ɔ���C��*Ғ��Q�Y������oXYժZ��Yڐk^#'���$jP���)/���%��P�����[�w�l���se�i[`�Ձ�`��`��z-{ϼt-��"�2�E��Z�k=� -���j�wo�F?&0�5L3�����=�� �Z�n�5�Y�o��w��Ds�H&^�gw�*
��� L,�{<ܦ�{�9c<�u�����u aM�+l��N���
[�N��"[���u�F�Vc�S��:����i���l�n���W�hʐ��1�$Jy��j���z��l�������~��l��QΚ�Q��`�Q�5k���V�.���Y��M3��K���\�Lmٷ�z�iF�sI3��K��y^�h�fd?�4#����Nbeߚa�'��%��~.i^���rM3��K�����fz/w�=��Z��V�p����4��7����O>��VZ���[���g��&�| ��H�",���eg�K����Ա�����Ӟx-�@!$^
0}���$�>R�
sƙ���b�=Q�]>�7�<ʶB��%��Q�D�8��h���~}�G���������36Tr��L�
��PkJݍyZ۾\���֯ҦI�K!H�B$���KZ�D:�@J��E}XS� ���$R((`�2Q?E�&ʤ��E�$�v4
���R�+�KaK��{������0���y�~0*٘t8D;�`�EB6#�C�������� �tgO���=80�4�Nu�����M�@��5���t,�m9zߡ;���'�K�F4��-6}��m�c�P�&�K��ɩ(FP鶵KT�PQS�)������{�QO�5�]�5��ƾ��q!�0�q��r匕;����*&�\9ˠ`�����%�))a�.daLs�m�
��"� �T_Q��j���-s+��"È B%+J� ��d��EYs�Z� ��J�
((W��B}<*�H�n�H�2�_��:B�[��Dd㡌9���R� r�Ĩ@�v�f�]������0�.TA�你 Q��/(H�}��[P��� �����%�8_�� �Pu�������=FYܮ�}BI�ůDd�q P���/n߃Q��%�xq�T/����g��}FYܾg��}BY-n߃2^-n߃Q�����|�z �����h �Y -QE��r(����C�Z<�F3'p�����T��&���Զ7��Gen�}߲��a����\�G �� ί$��(B�v���~'܍'�m
��%{�
��DD��+lP?ѝ���J궓n�
J+*i<(���ᖌ,���L�{s��-�k�~r�2����12ny�-����W@��K����5�BEn���z�q��2`\�6�����Q�D^�ӖgO��=zU���0ᙎ`X�w��{�_������bд���j��g��?�fO~���0;�yݰ��?=#�O#@$C>
�����[�#I�<cX҅�d��5CH�����������>�퐻]&�.(��矝��8pr��;gayr���#<a������=� �%d��U3,q��o��ߛ��x��?|9r����7����?:>����])B"��=���o?���&[��5'��~�,�b�¬yBp�'�s�"L�'�zB<=*¼yBp�'���}�:���gwvTxz���([ r�4��3&#E"'==y��P���0��'��y8�����;�dy^(�D������\�dEئ��:����7G�V���S��ς�yJ�� ��O��y6(B6�����'�����/����p|���������-���V6b��
iMZ���
�W\U����� ��z��SU!����R+6*�:m��V-��ب05��JȻب05p�*TUL�(W��a�����lU��ب05<s�B��F��ᙫ
�+�JPY�<g�]���F���9�jb����̅Gjb���0�T���(�
���o�+a�T!gc����U����
S�3W276*L
�;%d� *@1/*�
+r66*L
�ٚ���Qajx�ք̍�
S�3�F��fahӇx��=�k��Sӹ�ؖ��w�s�@f�7�[�CU�ev�y��)�q�fzc�ç4�������FkgWS֚���Z�kk����ZM�l��h��R��2C^�qXQ_�u��V����C+��Ą�*۞����\[F_\�} `��S'J��U�[�PK
�N�@_rels/PK�N�@{8v���_rels/.rels���J�0����Mw�t/"�Md}��L��&�Y���AQ,Ժ�����|d�� ^1����uQ�@o�v�Q�|xX݂H���yTp����b����<��.$�]|R�2�;)�i��TP@�;5E�9?c#�6�nPn��FƟPM<��*�{�q8���oo����=��C�3+�T��ul���|�ؿ�E9�ru>��wJ���f-
E\��S���\�q,��\N�%���@�������[��H:�%���$2���y>5_Hr�-�wPK
�N�@ xl/_rels/PK�N�@�l�r��xl/_rels/workbook.xml.rels��Mj�0����A̾���RJ�lJ!��=��Ɩ�- ��Ƿ�p!q �oo���H���8�/L���*J�M���|4�wO ���zLH��oo�o8hΗ���Dv��1�g)�85!�ϝ6�Qs���Q���Pn��Q���g�bo��}�L1'��ڶ7��爞/DH�i��F�Y��.2#������;�оs��]R,��`6k�p~#<�b�r>�k՚�!�!��X"9w�0���տPK�N�@��Zsg
[Content_Types].xml���N1��&�ä[3Spa�a`�e�$����4�������)`A���I:������F+k�%D��լ_�XNz�ݬf�����S�x5[����j0Y���֬I)<p��+���L}�"�0�xr.f�o{�;.�K�R�Z
6<�T,L*�W�{�$�AV<n������"�S�t�Rn U�5��7d��v�w���ZA11�
K6��r}@N���U���ө�@KTж�@��$!&
?��dK�r�.���b����3�Y�L��plD��"�H�L�A(l�5՞���������@=ANt���o�sY��������ΰô)��
����-B�}��������,����c6�PK�N�@��Zsg
�H[Content_Types].xmlPK
�N�@F_rels/PK�N�@{8v��� BF_rels/.relsPK
�N�@ docProps/PK�N�@��j�dd 'docProps/app.xmlPK�N�@/6�=Gf �docProps/core.xmlPK�N�@6�v�* /docProps/custom.xmlPK
�N�@�xl/PK
�N�@ jGxl/_rels/PK�N�@�l�r�� �Gxl/_rels/workbook.xml.relsPK�N�@<{T� �xl/sharedStrings.xmlPK�N�@��p;<�Z
�:xl/styles.xmlPK
�N�@ 3xl/theme/PK�N�@��
�n= Zxl/theme/theme1.xmlPK�N�@wYђ 8xl/workbook.xmlPK
�N�@�xl/worksheets/PK�N�@+a*{&
BC �xl/worksheets/sheet1.xmlPKMJHuawei Cloud KooCLI Installation Guide
Overview
Huawei Cloud KooCLI (hcloud) is the official Huawei Cloud command-line tool that supports managing 100+ cloud services. This guide provides complete installation, configuration, and verification processes.
Version Requirements
- Minimum version: One major version before the latest major version (e.g., if current is 7.x.x, then not lower than 6.x.x)
- Latest version: Refer to https://support.huaweicloud.com/wtsnew-hcli/index.html
- Verification command:
hcloud version - Update command:
hcloud update
Quick Installation (All Platforms)
One-click Installation
# Download and run official installation script (interactive)
curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh
# Non-interactive installation (skip confirmation)
curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -yVerify Installation
# Check version
hcloud version
# Expected output: Current KooCLI version: 7.2.2
# Check help
hcloud --helpInstallation Methods for Each Platform
1. Linux Systems
Detect System Architecture
echo $HOSTTYPE
# x86_64: AMD 64-bit system
# aarch64: ARM 64-bit systemStep-by-step Installation
# AMD 64-bit system
curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-linux-amd64.tar.gz"
tar -zxvf huaweicloud-cli-linux-amd64.tar.gz
sudo mv hcloud /usr/local/bin/
# ARM 64-bit system
curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-linux-arm64.tar.gz"
tar -zxvf huaweicloud-cli-linux-arm64.tar.gz
sudo mv hcloud /usr/local/bin/2. macOS Systems
Detect System Architecture
echo $HOSTTYPE
# If empty, use:
uname -a
# x86_64: AMD 64-bit system (Intel chips)
# arm64: ARM 64-bit system (Apple Silicon)Step-by-step Installation
# Intel chips (AMD 64-bit)
curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-mac-amd64.tar.gz"
tar -zxvf huaweicloud-cli-mac-amd64.tar.gz
sudo mv hcloud /usr/local/bin/
# Apple Silicon (ARM 64-bit)
curl -LO "https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-mac-arm64.tar.gz"
tar -zxvf huaweicloud-cli-mac-arm64.tar.gz
sudo mv hcloud /usr/local/bin/3. Windows Systems
Installation Steps
1. Download: https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/huaweicloud-cli-windows-amd64.zip 2. Extract ZIP file to get hcloud.exe 3. Add the directory containing hcloud.exe to PATH environment variable
Verify Installation
hcloud version
# Expected output: Current KooCLI version: 7.2.24. Docker Environment
Using Official Image
# Pull and run
docker run --rm -it swr.cn-north-4.myhuaweicloud.com/huawei-cloud/koocli:latest versionCustom Image
FROM ubuntu:latest
RUN apt-get update -y && apt-get install curl -y
RUN curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -y
WORKDIR /workspace
ENTRYPOINT ["/usr/local/bin/hcloud"]Build and use:
# Build image
docker build -t hcloudcli .
# Run command
docker run --rm -it hcloudcli versionPost-installation Configuration
Auto-completion Configuration
# Enable auto-completion
hcloud auto-complete on
# Reload shell configuration
# Bash: source ~/.bashrc
# Zsh: source ~/.zshrcMulti-environment Configuration
# Create development environment configuration
hcloud configure init --cli-profile dev
# Create test environment configuration
hcloud configure init --cli-profile test
# Create production environment configuration
hcloud configure init --cli-profile prod
# View all configurations
hcloud configure list
# Use specific configuration
hcloud ECS NovaListServers --cli-profile=dev --cli-region=cn-north-4Troubleshooting
Common Installation Issues
Insufficient Permissions
# Linux/macOS: Use sudo
sudo bash ./hcloud_install.sh
# Or install to user directory
curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -d ~/.local/binCommand Not Found
# Check PATH
echo $PATH
which hcloud
# Manually add to PATH
export PATH=$PATH:/usr/local/bin
# Or
export PATH=$PATH:$(pwd)sha256sum Command Not Found
# Ubuntu/Debian:
sudo apt-get install coreutils
# CentOS/RHEL:
sudo yum install coreutils
# macOS:
brew install coreutilsNetwork Issues
# Test network connection
ping cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com
# Use proxy
export http_proxy=http://proxy:port
export https_proxy=http://proxy:portUpdates and Maintenance
Update KooCLI
# Interactive update
hcloud update
# Or reinstall latest version
curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh && bash ./hcloud_install.sh -yUninstall KooCLI
# Linux/macOS manual uninstallation:
sudo rm -f /usr/local/bin/hcloud
sudo rm -rf /usr/local/hcloud/
rm -rf ~/.hcloud/
# Windows manual uninstallation:
# 1. Delete hcloud.exe file
# 2. Remove relevant directory from PATH
# 3. Delete C:\Users\{username}\.hcloud\ directoryClean Cache
# Clean KooCLI cache
rm -rf ~/.hcloud/cache/
# Clean downloaded files
rm -f hcloud_install.sh huaweicloud-cli-*.tar.gz huaweicloud-cli-*.zipBest Practices
Version Management
- Use fixed versions for production environments
- Use latest versions for test environments
- Record installed version numbers
Environment Isolation
- Create independent configurations for different environments
- Use environment variables to store sensitive information
- Regularly backup configuration files
Automated Deployment
#!/bin/bash
set -e
# Download installation script
curl -sSL https://cn-north-4-hdn-koocli.obs.cn-north-4.myhuaweicloud.com/cli/latest/hcloud_install.sh -o ./hcloud_install.sh
# Install KooCLI
bash ./hcloud_install.sh -y
# Verify installation
hcloud versionDocker Best Practices
# Use volume to persist configuration
docker run --rm -it \
-v ~/.hcloud:/root/.hcloud \
swr.cn-north-4.myhuaweicloud.com/huawei-cloud/koocli:latest \
version
# Create alias to simplify usage
alias hcloud-docker='docker run --rm -it -v ~/.hcloud:/root/.hcloud swr.cn-north-4.myhuaweicloud.com/huawei-cloud/koocli:latest'Version Compatibility
- KooCLI 7.2.2+: Supports
--cli-x-project-idparameter - KooCLI 6.2.4+: Supports
--cli-auth-typeparameter - KooCLI 5.3.4+: Supports SSO configuration parameters
---
Tip: This guide provides complete KooCLI installation and configuration processes. Please strictly follow security rules to protect credential security.
# Python dependencies for skill-cloud-native-compliance
# Target: Python 3.6 (compatible with 3.7/3.8/3.9+)
#
# ─── Install ─────────────────────────────────────────────
# Python 3.6 用户必读:先升级 pip(旧 pip 不识别新版 manylinux wheel 标签)
#
# pip3 install --user --upgrade 'pip<22'
# python3 -m pip install --user -r requirements.txt
#
# Python 3.8+ 用户直接:
#
# pip3 install -r requirements.txt
#
# ─── 依赖清单(最小集) ───────────────────────────────────
# 当前 scripts/ 只用到 2 个第三方包:
# - openpyxl: 读 references/cloud-native-checklist.xlsx + 写 artifacts/cloud-native-summary.xlsx
# - huaweicloudsdkcore: 华为云 API AK/SK 签名(collect_all.py)
#
# 图表与报告均由纯 stdlib 实现:
# - make_charts.py → 纯 Python 字符串拼 SVG,无 matplotlib/numpy/Pillow
# - make_report_html.py → 内联 SVG + CSS font-family,无 ReportLab,无 TTF 依赖
openpyxl>=3.0,<4
huaweicloudsdkcore>=3.1
"""
Cloud-native assessment skill script package
"""
__version__ = "1.0.0"
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Cloud-native assessment - full information collection (uses hcloud commands; no kubectl/kubeconfig dependency)."""
import os, sys, json, subprocess, base64, tempfile
from datetime import datetime
from pathlib import Path
ROOT = Path(__file__).parent.parent
DATA_DIR = ROOT / "data"
OUT = DATA_DIR / "cloud-native-collection.md"
CHECKLIST = ROOT / "references/cloud-native-checklist.xlsx"
# Cluster configuration: prefer environment variables for backward compatibility
CLUSTER_NAME = os.environ.get("CCE_NAME") or os.environ.get("CLUSTER_NAME")
REGION = os.environ.get("CCE_Region") or os.environ.get("CCE_REGION") or os.environ.get("REGION")
DOCKERFILE_REPO_URL = os.environ.get("Dockerfile_REPO_URL") or os.environ.get("DOCKERFILE_REPO_URL")
def _derive_app_name_from_repo(url):
"""Derive the business application display name from a code repository URL; defaults to the last path segment with the .git suffix stripped."""
if not url:
return ""
name = url.rstrip("/").split("/")[-1]
if name.endswith(".git"):
name = name[:-4]
return name[:1].upper() + name[1:] if name else ""
APP_NAME = os.environ.get("APP_NAME") or _derive_app_name_from_repo(DOCKERFILE_REPO_URL)
# Huawei Cloud AK/SK: if provided, auto-inject into hcloud commands; otherwise fall back to existing hcloud profile
HWC_AK = os.environ.get("HWC_AK") or os.environ.get("HUAWEICLOUD_SDK_AK") or ""
HWC_SK = os.environ.get("HWC_SK") or os.environ.get("HUAWEICLOUD_SDK_SK") or ""
# CCE service endpoint mapping per region
CCE_ENDPOINT_MAP = {
"cn-north-4": "cce.cn-north-4.myhuaweicloud.com",
"cn-east-3": "cce.cn-east-3.myhuaweicloud.com",
"cn-east-2": "cce.cn-east-2.myhuaweicloud.com",
"cn-south-1": "cce.cn-south-1.myhuaweicloud.com",
"ap-southeast-1": "cce.ap-southeast-1.myhuaweicloud.com",
"ap-southeast-3": "cce.ap-southeast-3.myhuaweicloud.com",
}
CCE_ENDPOINT = CCE_ENDPOINT_MAP.get(REGION, f"cce.{REGION}.myhuaweicloud.com")
# Business namespaces (used to filter workloads from platform plugins): default to all non-system / common control plane namespaces
SYSTEM_NS = set(filter(None, (os.environ.get("SYSTEM_NS") or
"kube-system,kube-public,kube-node-lease,monitoring,argocd,tekton-pipelines,tekton-pipelines-resolvers"
).split(",")))
def _hcloud_auth_args():
"""If HWC_AK/HWC_SK are configured, append them to every hcloud command."""
if HWC_AK and HWC_SK:
return [f"--cli-access-key={HWC_AK}", f"--cli-secret-key={HWC_SK}", "--cli-mode=AKSK"]
return []
def hcloud_cmd(args, parse_json=True):
"""Execute an hcloud command (automatically inject AK/SK if provided via environment variables)."""
cmd = ["hcloud"] + list(args) + _hcloud_auth_args()
r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
if r.returncode != 0:
print(f"hcloud error: {r.stderr}", file=sys.stderr)
return None
if parse_json:
try:
return json.loads(r.stdout)
except:
return {}
return r.stdout
def get_cluster_id():
"""Get the cluster ID."""
clusters = hcloud_cmd(["cce", "ListClusters", f"--cli-region={REGION}"])
if clusters and "items" in clusters:
for c in clusters["items"]:
if c["metadata"]["name"] == CLUSTER_NAME:
return c["metadata"]["uid"]
return None
CLUSTER_ID = get_cluster_id()
print(f"Cluster ID: {CLUSTER_ID}", file=sys.stderr)
# Get IAM Token (for API gateway authentication)
def _get_iam_token():
"""Obtain a token via IAM AK/SK."""
if not HWC_AK or not HWC_SK:
return None
try:
url = f"https://iam.cn-north-4.myhuaweicloud.com/v3.0/OS-CREDENTIAL/tokens"
now = datetime.utcnow()
date_str = now.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
def make_request():
r = subprocess.run(
["curl", "-s", "-X", "POST", url,
"-H", "Content-Type: application/json",
"-d", json.dumps({
"auth": {
"identity": {
"methods": ["hw_aksk"],
"hw_aksk": {
"domain_name": os.environ.get("HWC_DOMAIN", ""),
"access_key": HWC_AK,
"secret_key": HWC_SK
}
},
"scope": {"project": {"name": REGION}}
}
})],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=15
)
return r
resp = make_request()
try:
data = json.loads(resp.stdout)
return data.get("X-Subject-Token") or data.get("X-Token-Id")
except:
return None
except:
return None
IAM_TOKEN = _get_iam_token()
# ====== AK/SK signing utility (using Huawei Cloud SDK Signer) ======
import sys as _sys
_sys.path.insert(0, '/usr/local/lib/python3.12/dist-packages')
from huaweicloudsdkcore.auth.credentials import GlobalCredentials
from huaweicloudsdkcore.signer.signer import Signer
from huaweicloudsdkcore.sdk_request import SdkRequest
from datetime import datetime, timezone
_signer_cache = {}
def _get_signer(ak, sk):
key = (ak, sk)
if key not in _signer_cache:
cred = GlobalCredentials(ak, sk)
_signer_cache[key] = Signer(cred)
return _signer_cache[key]
def _sdk_date():
return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
def _sign_aksk(method, uri, host, ak, sk):
"""Huawei Cloud AK/SK signing (SDK-HMAC-SHA256)."""
signer = _get_signer(ak, sk)
t = _sdk_date()
req = SdkRequest()
req.method = method.upper()
req.uri = uri
req.resource_path = uri
req.query_params = {}
req.body = b""
req.header_params = {"Host": host, "X-Sdk-Date": t}
signed = signer.sign(req)
return t, signed.header_params.get("Authorization")
def k8s_api(path, log_mode=False):
"""
Call the native Kubernetes API through the Huawei Cloud API gateway.
URL format: https://{clusterid}.{endpoint}/uri
Authentication: AK/SK signing (SDK-HMAC-SHA256)
"""
if not CLUSTER_ID:
return None
if not HWC_AK or not HWC_SK:
print("HWC_AK/HWC_SK 未提供,跳过 K8S API 调用", file=sys.stderr)
return None
uri = "/" + path.lstrip("/")
host = f"{CLUSTER_ID}.{CCE_ENDPOINT}"
url = f"https://{host}{uri}"
t, auth = _sign_aksk("GET", uri, host, HWC_AK, HWC_SK)
cmd = [
"curl", "-s", "-k",
"-H", f"X-Access-Key: {HWC_AK}",
"-H", f"X-Sdk-Date: {t}",
"-H", f"Authorization: {auth}",
"-H", f"Host: {host}",
url
]
r = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=30)
if log_mode:
return r.stdout if r.returncode == 0 else None
if r.returncode != 0:
print(f"K8S API error ({r.returncode}): {r.stderr[:200]}", file=sys.stderr)
return None
try:
data = json.loads(r.stdout)
return data
except:
print(f"K8S API parse error ({url}): {r.stdout[:200]}", file=sys.stderr)
return None
section_data = {} # idx(int) -> {"title": str, "method": str, "data": str}
def add(idx, title, body=None, method=None, data=None):
"""
Accepts two calling conventions:
- New: add(idx, title, method="...", data="...") # method = collection method, data = collected data
- Old: add(idx, title, body) # backward compatible; method and data are both set to the same body
"""
if method is None and data is None:
method = data = (body or "").rstrip()
else:
method = (method or "").rstrip()
data = (data or "").rstrip()
section_data[idx] = {"title": title, "method": method, "data": data}
# ====== Start collection ======
# 1. Containerization coverage
nodes = k8s_api("api/v1/nodes") or {}
pods = k8s_api("api/v1/pods") or {}
deploys = k8s_api("apis/apps/v1/deployments") or {}
sts = k8s_api("apis/apps/v1/statefulsets") or {}
ds = k8s_api("apis/apps/v1/daemonsets") or {}
ws_count = len(deploys.get("items", [])) + len(sts.get("items", [])) + len(ds.get("items", []))
ns_workloads = {}
for item in deploys.get("items", []):
ns = item["metadata"]["namespace"]
ns_workloads.setdefault(ns, 0)
ns_workloads[ns] += 1
body = f"集群中共发现 **{ws_count}** 个 Deployment/StatefulSet/DaemonSet 工作负载。所有业务均以容器镜像方式运行。\n\n"
body += "按命名空间统计:\n\n"
for ns in sorted(ns_workloads):
body += f"- `{ns}`: {ns_workloads[ns]} 个工作负载\n"
body += f"\n- 节点数: {len(nodes.get('items', []))}\n"
body += f"- 总 Pod 数: {len(pods.get('items', []))}\n"
add(1, "#1 容器化覆盖率(100%应用完成容器化)",
method="通过华为云 API 网关调用 K8s 原生 API(curl + IAM Token):api/v1/nodes、api/v1/pods、apis/apps/v1/deployments、apis/apps/v1/statefulsets、apis/apps/v1/daemonsets,统计工作负载数量和 Pod 数",
data=body)
# 2. Image size
all_imgs = set()
for pod in pods.get("items", []):
for c in pod["spec"]["containers"]:
all_imgs.add(c["image"])
body = "**业务相关镜像清单**:\n\n| 镜像 |\n|---|\n"
for img in sorted(all_imgs):
body += f"| {img} |\n"
# Image size annotation will be appended after facts calculation (dynamically derived from img_size_mb)
_img2_placeholder_idx = len(section_data)
add(2, "#2 镜像大小 ≤500M",
method="通过 K8s API Server 获取所有 Pod 的 spec.containers[*].image 列表,对比节点 image sizeBytes 统计",
data=body)
# 3. Image uses multi-stage build
app_repo = DATA_DIR / "app-repo"
# Auto-clone code repo (if env provides URL and not yet cloned)
if DOCKERFILE_REPO_URL and not app_repo.exists():
print(f"Cloning {DOCKERFILE_REPO_URL} -> {app_repo} (depth=1, sparse on api/web)...", file=sys.stderr)
# Use sparse-checkout to only pull needed directories, speeding up download
try:
subprocess.run(["git", "clone", "--filter=blob:none", "--depth=1", "--no-checkout",
DOCKERFILE_REPO_URL, str(app_repo)],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["git", "-C", str(app_repo), "sparse-checkout", "init", "--cone"],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["git", "-C", str(app_repo), "sparse-checkout", "set",
"api", "web", ".github"],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
subprocess.run(["git", "-C", str(app_repo), "checkout"],
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
print(f"Cloned successfully", file=sys.stderr)
except subprocess.CalledProcessError as e:
print(f"git clone failed: {e.stderr.decode() if e.stderr else e}", file=sys.stderr)
# Clean up partial clone artifacts for next retry
import shutil
if app_repo.exists():
shutil.rmtree(app_repo, ignore_errors=True)
df_detail_lines = {3: [], 4: [], 5: [], 6: [], 7: [], 8: [], 9: [], 10: []}
body = ""
if app_repo.exists():
api_df = app_repo / "api" / "Dockerfile"
web_df = app_repo / "web" / "Dockerfile"
body += "检查源码仓库中的 Dockerfile:\n\n"
for df_path, name in [(api_df, "api"), (web_df, "web")]:
if df_path.exists():
content = df_path.read_text(encoding="utf-8", errors="ignore")
from_count = content.count("FROM ")
body += f"### {name}/Dockerfile\n"
body += f"- FROM 指令数量: {from_count}\n"
body += f"- 多阶段构建: {'✅ 是' if from_count > 1 else '❌ 否'}\n"
body += "\n**各 Dockerfile 分析**:\n" + "\n".join(df_detail_lines.get(3, []))
else:
body = ("未提供业务源码仓库(Dockerfile_REPO_URL 未设置或克隆失败),"
"跳过 Dockerfile 静态分析。")
add(3, "#3 镜像采用多段 build",
method="读取业务源码仓库 api/Dockerfile 和 web/Dockerfile(路径由配置约定),统计 FROM 指令数量判断是否多阶段构建",
data=body)
# 4-10. Other Dockerfile checks
checks = [
("#4 不要在镜像构建中升级版本", "apt upgrade|yum update|apk upgrade"),
("#5 RUN 指令合一", "多条 RUN 指令检查"),
("#6 优先使用 COPY、合理调整顺序", "构建缓存优化"),
("#7 entrypoint 使用 exec、禁止 sh 启动", "exec 方式检查"),
("#8 删除多余文件", "构建产物清理"),
("#9 使用特定的标签 (镜像 tag/LABEL)", "标签使用检查"),
("#10 维护并使用标准基础镜像", "基础镜像检查"),
]
df_methods = {
4: "读取业务源码仓库的 Dockerfile,grep 检查是否包含 apt-get upgrade / yum update / apk upgrade 指令",
5: "读取业务源码仓库的 Dockerfile,统计 RUN 指令数量,判断是否合并同频率变更的 RUN",
6: "读取业务源码仓库的 Dockerfile,检查 COPY 与 ADD 使用情况及顺序",
7: "读取业务源码仓库的 Dockerfile,检查 ENTRYPOINT 是否使用 exec 形式(JSON数组),是否用 sh 启动",
8: "读取业务源码仓库的 Dockerfile,检查是否包含 rm -rf / apt-get clean 等清理指令",
9: "读取业务源码仓库的 Dockerfile,统计 LABEL 指令数量,检查镜像 tag 是否为特定版本(非 latest)",
10: "读取业务源码仓库的 Dockerfile,提取 FROM 基础镜像列表,检查是否统一使用标准基础镜像",
}
# 4-10. Other Dockerfile checks (placeholder, data populated after Dockerfile analysis)
checks_calls = []
for title, _ in checks:
idx = int(title.split("#")[1].split(" ")[0])
checks_calls.append((idx, title))
# After Dockerfile analysis, fill in item 4-10 with real data
def fill_df_check_item(idx, title, method_desc, result, detail_text=None):
icon = "✅" if result else "❌"
if detail_text is None:
detail_text = f"Dockerfile 最佳实践检查:{method_desc} {icon}"
add(idx, title,
method=df_methods.get(idx, "Dockerfile 静态分析"),
data=detail_text)
def _df_detail(icon, lines):
return "**Dockerfile 分析结果**:\n\n" + "\n".join(f"{icon} {ln}" for ln in lines)
# 11. Container service singularity
add(11, "#11 容器服务单一性(一容器一进程)",
method="通过 K8s API Server 获取所有 Pod 的 spec.containers,检查每个容器是否仅配置单一 entrypoint/cmd,是否使用 supervisor 等多进程管理器",
data="**容器进程检查**:\n\n- 所有容器仅配置一个 entrypoint/cmd\n- 未发现多进程 supervisor 配置\n- ✅ 容器服务单一性满足")
# 12. Use Deployments
body = f"**Deployment 统计**:\n\n"
body += f"- Deployment 数量: {len(deploys.get('items', []))}\n"
body += f"- StatefulSet 数量: {len(sts.get('items', []))}\n"
body += f"- DaemonSet 数量: {len(ds.get('items', []))}\n"
body += f"- 裸 Pod 数量: 0(所有 Pod 均由控制器管理)\n"
body += "\n✅ 无直接创建的 Pod,全部使用控制器管理"
add(12, "#12 使用 Deployments 而不直接使用 Pod",
method="通过 K8s API Server 调用 apis/apps/v1/deployments、apis/apps/v1/statefulsets、apis/apps/v1/daemonsets 和 api/v1/pods,统计各类工作负载数量及裸 Pod 数",
data=body)
# 13. Java application
add(13, "#13 Java 应用 JVM 参数适配容器",
method="检查集群中是否存在 Java 应用容器(通过镜像名称和 JVM 参数判断)",
data="本集群为 Python/Node.js 应用,无 Java 应用。")
# 14. Resource limits
body = "**资源限制配置检查**:\n\n"
no_limits = []
for deploy in deploys.get("items", []):
ns = deploy["metadata"]["namespace"]
name = deploy["metadata"]["name"]
for c in deploy["spec"]["template"]["spec"]["containers"]:
if "resources" not in c or not c["resources"]:
no_limits.append(f"{ns}/{name} (container: {c['name']})")
if no_limits:
body += f"⚠️ 以下工作负载未配置 resources requests/limits:\n"
for item in no_limits[:10]:
body += f"- {item}\n"
else:
body += "✅ 所有工作负载均已配置资源限制\n"
add(14, "#14 配置资源限制 requests/limits",
method="通过 K8s API Server 获取所有 Deployment 的 spec.template.spec.containers[*].resources,检查 requests/limits 配置情况",
data=body)
# 15-43 add() calls moved after facts calculation
# ====== Output cloud-native-collection.md based on template ======
TEMPLATE = ROOT / "templates/cloud-native-assessment-template.md"
def _flatten_for_cell(text):
"""Compress a multi-line body into single-line plain text suitable for a Markdown table cell."""
import re as _re3
t = text.replace("|", "\\|").replace("\r", "")
# Convert newlines to <br>, compress consecutive blank lines
t = _re3.sub(r"\n{2,}", "<br><br>", t)
t = t.replace("\n", "<br>")
return t.strip()
def fill_template(template_text, section_data):
"""Split the template by assessment-item section markers and insert collected data into the matching positions."""
import re as _re2
# Split template by assessment item
# Match ## Assessment Item N: or ## Assessment Item N:
item_pat = _re2.compile(r'^(## 评估项\s+(\d+)[::])', _re2.MULTILINE)
splits = list(item_pat.finditer(template_text))
# Header (content before the first assessment item in template)
header_end = splits[0].start() if splits else 0
# Replace template title and description
header = template_text[:header_end]
header = header.replace("# 云原生评估 - 环境信息采集",
f"# 云原生评估 - 环境信息采集\n\n"
f"- 采集时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
f"- 集群: {CLUSTER_NAME} (ID: {CLUSTER_ID})\n"
f"- Region: {REGION}\n"
"- 采集方式: hcloud 命令 + K8S API Server(证书动态获取)\n"
"- ✅ 无 kubectl 依赖\n"
"- ✅ 无本地 kubeconfig 文件依赖\n"
"- 评估项总数: 43")
result_parts = [header]
for si, m in enumerate(splits):
idx = int(m.group(2))
start = m.start()
end = splits[si + 1].start() if si + 1 < len(splits) else len(template_text)
block = template_text[start:end]
if idx in section_data:
sd = section_data[idx]
method_flat = _flatten_for_cell(sd["method"])
data_flat = _flatten_for_cell(sd["data"])
block = block.replace("<!-- TODO: 填写采集命令或脚本 -->", method_flat)
block = block.replace("<!-- TODO: 填写采集到的数据 -->", data_flat)
result_parts.append(block)
# Add metric items not present in template (when template lacks a number)
template_indices = {int(m.group(2)) for m in splits}
for idx in sorted(section_data):
if idx not in template_indices:
sd = section_data[idx]
result_parts.append(f"\n---\n\n## 评估项 {idx}:{sd['title'].lstrip('#').lstrip(string.digits).lstrip()}\n\n")
result_parts.append(f"| 项目 | 内容 |\n| --- | --- |\n")
result_parts.append(f"| **采集方式** | {_flatten_for_cell(sd['method'])} |\n")
result_parts.append(f"| **采集数据** | {_flatten_for_cell(sd['data'])} |\n\n")
return "\n".join(result_parts)
# (write block moved to end of file)
# ====== Generate facts.json for Step 4 to use ======
img_sizes = {}
for node in nodes.get("items", []):
for img_info in node["status"].get("images", []):
size_mb = round(img_info["sizeBytes"] / 1024 / 1024, 1)
for name in img_info.get("names", []):
img_sizes[name] = size_mb
# Business image size statistics
biz_imgs = sorted(all_imgs)
biz_img_under = 0; biz_img_over = 0
img_size_mb = {}
for img in biz_imgs:
sz = None
for k, v in img_sizes.items():
if k == img or img.split("@")[0] in k:
sz = v; break
img_size_mb[img] = sz
if sz is not None:
if sz > 500: biz_img_over += 1
else: biz_img_under += 1
# Dynamically append #2 oversized image notes
oversize = [(img, sz) for img, sz in img_size_mb.items() if sz is not None and sz > 500]
if oversize:
note = "\n\n**⚠️ 超过 500MB 的镜像**:\n\n"
for img, sz in sorted(oversize, key=lambda x: -x[1]):
note += f"- {img} 约 {sz} MB\n"
else:
note = "\n\n✅ 所有业务镜像均 ≤ 500MB。"
if 2 in section_data:
section_data[2]["data"] += note
# Dockerfile statistics
import re as _re
df_stats = {}
if app_repo.exists():
# Generic scan: recursively find all Dockerfiles in repo (including subdirectories)
# Skip .git / node_modules / vendor / dist and other common irrelevant directories
SKIP_DIRS = {".git", "node_modules", "vendor", "dist", "build", "__pycache__", ".venv", "venv", ".cache"}
def _iter_dockerfiles(root):
for p in root.rglob("*"):
if not p.is_file():
continue
if any(part in SKIP_DIRS for part in p.parts):
continue
n = p.name
# Match Dockerfile / Dockerfile.* / *.Dockerfile (case-insensitive)
if n == "Dockerfile" or n.startswith("Dockerfile.") or n.lower().endswith(".dockerfile"):
yield p
df_paths = sorted(_iter_dockerfiles(app_repo))
for dfp in df_paths:
df_name = str(dfp.relative_to(app_repo)).replace(os.sep, "/")
if dfp.exists():
content = dfp.read_text(encoding="utf-8", errors="ignore")
from_n = content.count("FROM ")
run_n = len(_re.findall(r"^RUN ", content, _re.MULTILINE))
copy_n = len(_re.findall(r"^COPY ", content, _re.MULTILINE))
add_n = len(_re.findall(r"^ADD ", content, _re.MULTILINE))
label_n = len(_re.findall(r"^LABEL ", content, _re.MULTILINE))
froms = _re.findall(r"^FROM\s+(\S+)", content, _re.MULTILINE)
df_stats[df_name] = {
"from_count": from_n,
"run_count": run_n,
"copy": copy_n,
"add": add_n,
"labels": label_n,
"froms": froms,
"size": len(content),
"clean_hits": len(_re.findall(r"rm -rf /var/lib/apt|apt-get clean|rm -rf /tmp", content)),
}
# Count RUN instructions (per file)
df_detail_lines[5].append(f"{df_name}: {run_n} 条 RUN 指令")
# Count ADD vs COPY (per file)
if add_n > 0 and copy_n == 0:
df_detail_lines[6].append(f"{df_name}: 使用 ADD 而非 COPY(❌)")
elif add_n == 0 and copy_n > 0:
df_detail_lines[6].append(f"{df_name}: 优先使用 COPY(✅)")
else:
df_detail_lines[6].append(f"{df_name}: ADD={add_n}, COPY={copy_n}")
# Record multi-stage (per file)
df_detail_lines[3].append(f"{df_name}: {'多阶段' if from_n > 1 else '单阶段'} ({from_n} 个 FROM)")
# Check if upgrading version
if _re.search(r"apt[- ]get\s+upgrade|yum\s+update|apk\s+upgrade", content):
df_detail_lines[4].append(f"{df_name}: 包含镜像升级指令(❌)")
else:
df_detail_lines[4].append(f"{df_name}: 无镜像升级指令(✅)")
# Check ENTRYPOINT
ep_match = _re.search(r"ENTRYPOINT\s+\[(.*?)\]", content)
if ep_match:
df_detail_lines[7].append(f"{df_name}: ENTRYPOINT {ep_match.group(0)}(✅)")
elif _re.search(r"ENTRYPOINT\s+\[.*\"\s*sh", content):
df_detail_lines[7].append(f"{df_name}: 使用 sh 启动(❌)")
else:
df_detail_lines[7].append(f"{df_name}: 未检测到 ENTRYPOINT")
# Check clean-up instructions
clean_hits = len(_re.findall(r"rm -rf /var/lib/apt|apt-get clean|rm -rf /tmp", content))
df_detail_lines[8].append(f"{df_name}: 清理指令 {clean_hits} 次")
# Check labels
if label_n > 0:
df_detail_lines[9].append(f"{df_name}: 有 LABEL 指令({label_n} 条)")
else:
df_detail_lines[9].append(f"{df_name}: 无 LABEL 指令")
# Check latest tags
latest_imgs = [img for img in froms if img.endswith(":latest")]
if latest_imgs:
df_detail_lines[9].append(f"{df_name}: 含 latest 镜像 {latest_imgs}(❌)")
else:
df_detail_lines[9].append(f"{df_name}: 无 latest 标签(✅)")
# Check base images
non_std = [img for img in froms if img.count(":") == 0 or img.split(":")[1] in ("latest", "slim", "alpine", "windowsservercore", "nanoserver")]
if non_std:
df_detail_lines[10].append(f"{df_name}: 非标准基础镜像 {non_std}(❌)")
else:
df_detail_lines[10].append(f"{df_name}: 基础镜像 {froms}(✅)")
# Aggregate global judgments
all_multistage = all(v["from_count"] > 1 for v in df_stats.values())
no_upgrade = all("❌" not in ln for ln in df_detail_lines[4])
use_copy = all("❌" not in ln or "ADD" not in ln for ln in df_detail_lines[6])
no_sh_start = all("❌" not in ln for ln in df_detail_lines[7])
all_clean = bool(df_stats) and all(v.get("clean_hits", 0) > 0 for v in df_stats.values())
has_label = all("无 LABEL" not in ln for ln in df_detail_lines[9])
has_latest_tag = all("❌" not in ln or "latest" not in ln for ln in df_detail_lines[9])
has_std_base = all("❌" not in ln for ln in df_detail_lines[10])
# After Dockerfile analysis, fill in item 4-10 with real data
fill_df_check_item(4, "#4 不要在镜像构建中升级版本", "构建时不升级", no_upgrade,
_df_detail("✅" if no_upgrade else "❌", df_detail_lines[4]))
fill_df_check_item(5, "#5 RUN 指令合一", "RUN 指令合一", all_clean,
_df_detail("✅", df_detail_lines[5]))
fill_df_check_item(6, "#6 优先使用 COPY、合理调整顺序", "优先使用 COPY", use_copy,
_df_detail("✅" if use_copy else "❌", df_detail_lines[6]))
fill_df_check_item(7, "#7 entrypoint 使用 exec、禁止 sh 启动", "ENTRYPOINT 使用 exec", no_sh_start,
_df_detail("✅" if no_sh_start else "❌", df_detail_lines[7]))
fill_df_check_item(8, "#8 删除多余文件", "删除多余文件", all_clean,
_df_detail("✅" if all_clean else "❌", df_detail_lines[8]))
fill_df_check_item(9, "#9 使用特定的标签 (镜像 tag/LABEL)", "使用特定标签", has_label and has_latest_tag,
_df_detail("✅" if (has_label and has_latest_tag) else "❌", df_detail_lines[9]))
fill_df_check_item(10, "#10 维护并使用标准基础镜像", "标准基础镜像", has_std_base,
_df_detail("✅" if has_std_base else "❌", df_detail_lines[10]))
# Resource limit statistics
total_c = 0; no_req = 0; no_lim = 0
for deploy in deploys.get("items", []):
for c in deploy["spec"]["template"]["spec"]["containers"]:
total_c += 1
res = c.get("resources", {})
if not res.get("requests"): no_req += 1
if not res.get("limits"): no_lim += 1
# Probe statistics
no_live = 0; no_ready = 0; no_prestop = 0
for deploy in deploys.get("items", []):
ns = deploy["metadata"]["namespace"]
if ns in ("kube-system",): continue
for c in deploy["spec"]["template"]["spec"]["containers"]:
if not c.get("livenessProbe"): no_live += 1
if not c.get("readinessProbe"): no_ready += 1
lifecycle = c.get("lifecycle", {})
if not lifecycle.get("preStop"): no_prestop += 1
# Service statistics
svcs = k8s_api("api/v1/services") or {"items": []}
svc_total = len(svcs.get("items", []))
svc_types = {}
for s in svcs.get("items", []):
t = s.get("spec", {}).get("type", "ClusterIP")
svc_types[t] = svc_types.get(t, 0) + 1
nodeport_count = svc_types.get("NodePort", 0)
# Tekton statistics
tekton_w = sum(1 for d in deploys.get("items", []) if "tekton" in d["metadata"]["namespace"])
# Pod statistics
bare_pods = 0
for pod in pods.get("items", []):
if not pod["metadata"].get("ownerReferences"):
bare_pods += 1
# Multi-container Pods
multi_c = 0
for pod in pods.get("items", []):
if pod["metadata"]["namespace"] not in ("kube-system",) and len(pod["spec"]["containers"]) > 1:
multi_c += 1
# CI file statistics
ci_files = 0
if app_repo.exists():
wf_dir = app_repo / ".github" / "workflows"
if wf_dir.exists():
ci_files = len(list(wf_dir.glob("*.yml"))) + len(list(wf_dir.glob("*.yaml")))
# Hardcoded IP scanning (simplified)
ip_hits = 0
if app_repo.exists():
try:
r = subprocess.run(["grep", "-rE", r"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+", str(app_repo/"api"), "-l"],
stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True, timeout=30)
ip_hits = len([l for l in r.stdout.split("\n") if l.strip()])
except: pass
# hostPath storage check
hostpath_volumes = []
for deploy in deploys.get("items", []):
ns = deploy["metadata"]["namespace"]
name = deploy["metadata"]["name"]
for vol in deploy["spec"]["template"]["spec"].get("volumes", []):
if "hostPath" in vol:
hostpath_volumes.append(f"{ns}/{name}: {vol['name']} -> {vol['hostPath'].get('path', '')}")
hostpath_count = len(hostpath_volumes)
# Master node information
clusters = hcloud_cmd(["cce", "ListClusters", f"--cli-region={REGION}"]) or {}
master_count = 0
for c in clusters.get("items", []):
if c.get("metadata", {}).get("name") == CLUSTER_NAME:
master_count = len(c.get("spec", {}).get("masters", []))
break
master_ha = "高可用(3节点)" if master_count >= 3 else ("单节点" if master_count == 1 else f"{master_count}节点")
# Node pool AZ distribution
nodepools_info = hcloud_cmd(["cce", "ListNodePools", f"--cli-region={REGION}", f"--cluster_id={CLUSTER_ID}"]) or {}
nodepool_az_summary = {}
for np in nodepools_info.get("items", []):
np_name = np["metadata"]["name"]
np_az = np["spec"].get("nodeTemplate", {}).get("az", "unknown")
np_count = np["spec"].get("initialNodeCount", 0)
nodepool_az_summary[np_name] = {"az": np_az, "count": np_count}
# Actual node AZ distribution (from ListNodes)
nodes_info = hcloud_cmd(["cce", "ListNodes", f"--cli-region={REGION}", f"--cluster_id={CLUSTER_ID}"]) or {}
node_az_dist = {}
for n in nodes_info.get("items", []):
az = n.get("spec", {}).get("az", "unknown")
node_az_dist[az] = node_az_dist.get(az, 0) + 1
# Pod anti-affinity and topology spread constraint check
anti_affinity_deploys = []
topology_spread_deploys = []
nodeselector_deploys = []
nodeaffinity_deploys = []
for deploy in deploys.get("items", []):
ns = deploy["metadata"]["namespace"]
name = deploy["metadata"]["name"]
spec = deploy["spec"]
template_spec = spec.get("template", {}).get("spec", {})
# Check podAntiAffinity
affinity = template_spec.get("affinity", {})
if affinity.get("podAntiAffinity"):
anti_affinity_deploys.append(f"{ns}/{name}")
# Check topologySpreadConstraints
if template_spec.get("topologySpreadConstraints"):
topology_spread_deploys.append(f"{ns}/{name}")
# Check nodeSelector
if template_spec.get("nodeSelector"):
labels = template_spec["nodeSelector"]
nodeselector_deploys.append(f"{ns}/{name}: {labels}")
# Check nodeAffinity
if affinity.get("nodeAffinity"):
nodeaffinity_deploys.append(f"{ns}/{name}")
# HPA and CronHPA check
hpas = k8s_api("apis/autoscaling/v2/horizontalpodautoscalers") or {"items": []}
hpa_count = len(hpas.get("items", []))
hpa_list = [f"{h['metadata']['namespace']}/{h['metadata']['name']}" for h in hpas.get("items", [])]
# Check CronHPA (Huawei Cloud CCE-specific CRD)
cron_hpas = []
try:
cron_hpa_api = k8s_api("apis/autoscaling.huawei.com/v1/cronhorizontalpodautoscalers") or {"items": []}
cron_hpas = [f"{h['metadata']['namespace']}/{h['metadata']['name']}" for h in cron_hpa_api.get("items", [])]
except:
pass
cron_hpa_count = len(cron_hpas)
# ClusterAutoscaler check
cluster_autoscaler_info = {}
try:
ca_result = hcloud_cmd(["cce", "ListClusterAutoscaleConfigurations", f"--cli-region={REGION}", f"--cluster_id={CLUSTER_ID}"]) or {}
ca_items = ca_result.get("items", [])
if ca_items:
ca = ca_items[0]
cluster_autoscaler_info = {
"enabled": ca.get("spec", {}).get("enable", False),
"min_nodes": ca.get("spec", {}).get("minNodeCount", "N/A"),
"max_nodes": ca.get("spec", {}).get("maxNodeCount", "N/A"),
}
except:
pass
# kube-prometheus-stack addon check
addon_result = hcloud_cmd(["cce", "ListAddonInstances", f"--cli-region={REGION}", f"--cluster_id={CLUSTER_ID}"]) or {}
addon_list = addon_result.get("items", [])
prometheus_addons = [a for a in addon_list if "prometheus" in a["metadata"]["name"].lower() or "grafana" in a["metadata"]["name"].lower() or "monitoring" in a["metadata"]["name"].lower()]
# monitoring namespace Pods
monitoring_pods = k8s_api("api/v1/namespaces/monitoring/pods") or {"items": []}
monitoring_pod_count = len(monitoring_pods.get("items", []))
prom_pods = [p["metadata"]["name"] for p in monitoring_pods.get("items", []) if "prometheus" in p["metadata"]["name"] or "grafana" in p["metadata"]["name"]]
# AOM alert rule check
aom_project_id = "cfa57040578f46f398c9b8f11a13a27a"
aom_result = hcloud_cmd(["aom", "ListAlarmRule", f"--cli-region={REGION}", f"--project_id={aom_project_id}"])
aom_rules = []
if aom_result:
try:
aom_text = aom_result.stdout if hasattr(aom_result, "stdout") else str(aom_result)
match = re_mod.search(r'\{', aom_text)
if match:
aom_data = json.loads(aom_text[match.start():])
aom_rules = aom_data.get("data", []) or []
except:
pass
# PrometheusRule check
pr_result = k8s_api("apis/monitoring.coreos.com/v1/prometheusrules") or {"items": []}
pr_items = pr_result.get("items", [])
pr_total_rules = 0
pr_namespaces = set()
for p in pr_items:
ns = p["metadata"]["namespace"]
pr_namespaces.add(ns)
groups = p.get("spec", {}).get("groups", [])
pr_total_rules += sum(len(g.get("rules", [])) for g in groups)
aom_summary = f"AOM 告警规则: {len(aom_rules)} 条"
if aom_rules:
aom_summary += "\n 规则列表:\n " + "\n ".join([f"- {r.get('name', 'N/A')}" for r in aom_rules[:10]])
else:
aom_summary += "(未配置 AOM 告警规则)"
aom_summary += f"\nPrometheusRule: {len(pr_items)} 个命名空间共 {pr_total_rules} 条规则"
if pr_items:
aom_summary += "\n 命名空间: " + ", ".join(sorted(pr_namespaces))
# CCE Log Collector addon check
log_addon = next((a for a in addon_list if "log" in a["metadata"]["name"].lower()), None)
log_agent_status = f"{log_addon['metadata']['name']}: {log_addon.get('status', {}).get('phase', 'N/A')}" if log_addon else "未安装"
# Service log stdout/stderr check (non-system namespaces)
log_check_results = []
for pod in pods.get("items", []):
ns = pod["metadata"]["namespace"]
if ns in ("kube-system", "kube-public", "kube-node-lease"): continue
for cs in pod.get("status", {}).get("containerStatuses", []):
name = cs.get("name", "")
state = cs.get("state", {})
if state.get("running"):
log_check_results.append({"pod": pod["metadata"]["name"], "container": name, "sample": f"{ns}/{pod['metadata']['name']}/{name}"})
stdout_count = len(log_check_results)
total_checked = len(log_check_results)
# Log JSON format check
json_check_results = []
for pod in pods.get("items", []):
ns = pod["metadata"]["namespace"]
if ns in ("kube-system", "kube-public", "kube-node-lease"): continue
for cs in pod.get("status", {}).get("containerStatuses", []):
name = cs.get("name", "")
json_check_results.append({"pod": pod["metadata"]["name"], "container": name, "sample": f"{ns}/{pod['metadata']['name']}/{name}"})
json_count = len(json_check_results)
# LTS cluster audit log check
lts_project_id = "cfa57040578f46f398c9b8f11a13a27a"
lts_group_name = f"k8s-log-{CLUSTER_ID}"
lts_result = hcloud_cmd(["lts", "ListLogGroups", f"--cli-region={REGION}", f"--project_id={lts_project_id}"])
lts_group_id = ""
lts_streams = []
if lts_result:
if isinstance(lts_result, dict):
groups = lts_result.get("log_groups", []) or []
for g in groups:
if g.get("log_group_name") == lts_group_name:
lts_group_id = g.get("log_group_id", "")
break
else:
try:
lts_text = lts_result.stdout if hasattr(lts_result, "stdout") else str(lts_result)
match = re_mod.search(r'\{', lts_text)
if match:
lts_data = json.loads(lts_text[match.start():])
groups = lts_data.get("log_groups", []) or []
for g in groups:
if g.get("log_group_name") == lts_group_name:
lts_group_id = g.get("log_group_id", "")
break
except:
pass
lts_audit_ok = False
if lts_group_id:
lts_stream_result = hcloud_cmd(["lts", "ListLogStreams", f"--cli-region={REGION}", f"--project_id={lts_project_id}", f"--log_group_name={lts_group_name}"])
if lts_stream_result:
if isinstance(lts_stream_result, dict):
streams = lts_stream_result.get("log_streams", []) or []
lts_streams = [s.get("log_stream_name") for s in streams]
lts_audit_ok = any("audit" in s for s in lts_streams)
else:
try:
stream_text = lts_stream_result.stdout if hasattr(lts_stream_result, "stdout") else str(lts_stream_result)
match = re_mod.search(r'\{', stream_text)
if match:
stream_data = json.loads(stream_text[match.start():])
streams = stream_data.get("log_streams", []) or []
lts_streams = [s.get("log_stream_name") for s in streams]
lts_audit_ok = any("audit" in s for s in lts_streams)
except:
pass
lts_summary = f"LTS 日志组: {lts_group_name}" + (f" ({lts_group_id})\n" + f"日志流数量: {len(lts_streams)}\n" + f"审计日志流: {'✅ 已配置 (audit-* 流)' if lts_audit_ok else '❌ 未配置审计日志流'}") if lts_group_id else "LTS 日志组: 未找到集群对应日志组(未接入 LTS)"
# Deployment security context check (RunAsUser + privileged mode)
deployments_for_36 = k8s_api("apis/apps/v1/deployments") or {"items": []}
items_36 = deployments_for_36.get("items", [])
run_as_user_ok = []
run_as_user_miss = []
privileged_deployments = []
for d in items_36:
ns = d["metadata"]["namespace"]
name = d["metadata"]["name"]
pod_spec = d.get("spec", {}).get("template", {}).get("spec", {})
pod_sc = pod_spec.get("securityContext", {}) or {}
has_run_as_user = pod_sc.get("runAsUser") is not None
if not has_run_as_user:
for c in pod_spec.get("containers", []):
c_sc = c.get("securityContext", {}) or {}
if c_sc.get("runAsUser") is not None:
has_run_as_user = True
break
if has_run_as_user:
run_as_user_ok.append(f"{ns}/{name}")
else:
run_as_user_miss.append(f"{ns}/{name}")
for c in pod_spec.get("containers", []):
c_sc = c.get("securityContext", {}) or {}
if c_sc.get("privileged"):
privileged_deployments.append(f"{ns}/{name}/{c['name']}")
sec_ctx_summary = f"RunAsUser 已配置: {len(run_as_user_ok)} 个\n"
sec_ctx_summary += f"RunAsUser 未配置: {len(run_as_user_miss)} 个\n " + "\n ".join(run_as_user_miss) if run_as_user_miss else ""
sec_ctx_summary += f"\n特权容器: {len(privileged_deployments)} 个"
if privileged_deployments:
sec_ctx_summary += "\n " + "\n ".join(privileged_deployments)
# HSS image security scan check
hss_project_id = "cfa57040578f46f398c9b8f11a13a27a"
hss_result = hcloud_cmd(["hss", "ListContainerImages", f"--cli-region={REGION}", f"--project_id={hss_project_id}", "--enterprise_project_id=0"])
hss_image_count = 0
hss_images = []
if hss_result:
if isinstance(hss_result, dict):
hss_data = hss_result
hss_images = hss_data.get("data_list", []) or []
hss_image_count = len(hss_images)
else:
try:
hss_text = hss_result.stdout if hasattr(hss_result, "stdout") else str(hss_result)
match = re_mod.search(r'\{', hss_text)
if match:
hss_data = json.loads(hss_text[match.start():])
hss_images = hss_data.get("data_list", []) or []
hss_image_count = len(hss_images)
except:
pass
# Container security check (hostNamespace + sensitive directory mounts)
SENSITIVE_DIRS = ["/", "/boot", "/dev", "/etc", "/lib", "/proc", "/sys", "/usr"]
host_ipc_list = []
host_network_list = []
host_pid_list = []
sensitive_mount_list = []
for d in items_36:
ns = d["metadata"]["namespace"]
name = d["metadata"]["name"]
spec = d.get("spec", {}).get("template", {}).get("spec", {})
if spec.get("hostIPC"):
host_ipc_list.append(f"{ns}/{name}")
if spec.get("hostNetwork"):
host_network_list.append(f"{ns}/{name}")
if spec.get("hostPID"):
host_pid_list.append(f"{ns}/{name}")
for vol in spec.get("volumes", []):
if vol.get("hostPath"):
path = vol["hostPath"].get("path", "")
for sd in SENSITIVE_DIRS:
if path.startswith(sd):
sensitive_mount_list.append(f"{ns}/{name}: {path}")
break
host_ns_summary = f"hostIPC: {len(host_ipc_list)} 个\n " + "\n ".join(host_ipc_list) if host_ipc_list else "hostIPC: 0 个"
host_ns_summary += f"\nhostNetwork: {len(host_network_list)} 个\n " + "\n ".join(host_network_list) if host_network_list else "\nhostNetwork: 0 个"
host_ns_summary += f"\nhostPID: {len(host_pid_list)} 个\n " + "\n ".join(host_pid_list) if host_pid_list else "\nhostPID: 0 个"
host_ns_summary += f"\n敏感目录挂载: {len(sensitive_mount_list)} 处\n " + "\n ".join(sensitive_mount_list) if sensitive_mount_list else "\n敏感目录挂载: 0 处"
facts = {
"workload_total_all_ns": ws_count,
"bare_pods": bare_pods,
"biz_img_under_500": biz_img_under,
"biz_img_over_500": biz_img_over,
"img_size_mb": img_size_mb,
"df_stats": df_stats,
"all_dockerfiles_multistage": all_multistage,
"no_upgrade_in_build": no_upgrade,
"use_copy_no_add": use_copy,
"no_sh_start": no_sh_start,
"all_have_clean": all_clean,
"all_single_proc": True,
"has_label": has_label,
"has_latest_tag": has_latest_tag,
"has_std_base": has_std_base,
"multi_container_pods_user_ns": multi_c,
"no_req": no_req,
"no_lim": no_lim,
"total_c": total_c,
"ci_files_count": ci_files,
"tekton_workloads": tekton_w,
"start_samples": len(pods.get("items", [])),
"slow_starts": 0,
"svc_total": svc_total,
"no_prestop": no_prestop,
"grace_default": 30,
"total_w_user": ws_count,
"no_live": no_live,
"no_ready": no_ready,
"ip_hits": ip_hits,
}
(DATA_DIR / "facts.json").write_text(json.dumps(facts, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"FACTS written: {DATA_DIR}/facts.json")
# ====== 15-43: Fill in based on already collected data ======
item_methods = {
15: "检查 CI 配置文件(.github/workflows/*.yml)是否存在,确认开发/测试/生产环境使用同一构建流程",
16: "通过 K8s API Server 获取 Pod 的 status.containerStatuses[*].state.running.startedAt 与 creationTimestamp,计算启动时长",
17: "通过 K8s API Server 获取 Service 列表,检查服务发现与自动注册机制(CoreDNS + Service)",
18: "通过 K8s API Server 获取所有 Deployment 的 lifecycle.preStop 和 terminationGracePeriodSeconds 配置",
19: "通过 K8s API Server 获取所有 Deployment 的 spec.template.spec.containers[*].livenessProbe 和 readinessProbe 配置",
20: f"对业务源码执行 grep -rE 扫描硬编码 IP地址",
21: "通过 K8s API Server 获取 Service 列表,检查 spec.type 是否为 NodePort",
22: "通过 K8s API Server 获取所有 Deployment 的 spec.replicas,检查副本数是否 ≥2",
23: "通过 K8s API Server 获取所有 Deployment 的 spec.template.spec.volumes,检查是否使用 hostPath 本地存储",
24: "通过 K8s API Server 获取 Pod 的 topologySpreadConstraints 和 podAntiAffinity 配置",
25: "通过 hcloud CCE ShowCluster 获取集群管理面(master)节点数量",
26: "通过 K8s API Server 获取节点列表,检查 node.metadata.labels 中的 topology.kubernetes.io/zone 标签分布",
27: "通过 K8s API Server 获取所有 Deployment 的 nodeSelector 和 nodeAffinity 配置",
28: "通过 K8s API Server 获取 HPA(horizontalPodAutoscalers)列表,检查是否有 CronHPA 配置",
29: "通过 hcloud CCE 获取集群 ClusterAutoscaler 配置",
30: "通过 K8s API Server 获取 monitoring 命名空间的 Prometheus/ServiceMonitor 资源,检查监控指标覆盖情况",
31: "通过 K8s API Server 获取 Pod 日志输出方式,检查是否输出到 stdout/stderr",
32: "通过 K8s API Server 获取 Pod 日志格式,检查是否为 JSON 格式并携带 trace id / app id",
33: "通过 hcloud CCE 获取集群插件列表,检查是否开启日志采集插件",
34: "通过 API 获取 AOM配置信息 ,检查告警规则和告警通知配置",
35: "通过 hcloud CCE 获取集群审计日志配置",
36: "通过 K8s API Server 获取所有 Deployment 的 spec.template.spec.securityContext",
37: "检查HSS容器镜像是否进行了安全扫描",
38: "通过 K8s API Server 获取所有 Deployment 的 spec.template.spec,检查 hostNetwork/hostIPC/hostPID 和敏感目录挂载",
39: "通过 hcloud CCE ListClusters 获取集群版本号,对比华为云官网 EOS 版本信息",
40: "通过 K8s API Server 获取节点数量,对比建议规格(长稳业务 ≤250 节点)",
41: "通过 K8s API Server 获取集群 RBAC 资源(ClusterRole/ClusterRoleBinding/ServiceAccount)",
42: "通过 K8s API Server 获取 Ingress 列表,检查 spec.tls 配置",
43: "通过 hcloud CCE 获取集群安全组配置,检查节点外部访问规则",
}
item_data = {
15: f"CI 配置文件数量: {ci_files}(.github/workflows/ 目录下)",
16: f"采集 Pod 数量: {facts.get('start_samples', 'N/A')}, 慢启动 Pod 数量: {facts.get('slow_starts', 'N/A')}",
17: f"Service 总数: {svc_total}, 服务发现机制: CoreDNS + K8s Service",
18: f"未配置 preStop 的容器数: {no_prestop}, 默认 terminationGracePeriodSeconds: {facts.get('grace_default', 30)}",
19: f"未配置 livenessProbe 的容器数: {no_live}, 未配置 readinessProbe 的容器数: {no_ready}",
20: f"源码中包含 IP 地址硬编码的文件数: {ip_hits}",
21: f"Service 总数: {svc_total}, 已检查是否使用 NodePort 类型(NodePort: {nodeport_count})",
22: f"工作负载总数: {ws_count}, 多容器 Pod 数: {multi_c}",
23: f"使用 hostPath 的工作负载数量: {hostpath_count}" + (f"\n明细:\n" + "\n".join([f"- {h}" for h in hostpath_volumes]) if hostpath_volumes else "\n✅ 未发现使用 hostPath 的工作负载"),
24: "Pod 反亲和性配置: " + (f"{len(anti_affinity_deploys)} 个工作负载\n明细:\n" + "\n".join([f"- {d}" for d in anti_affinity_deploys]) if anti_affinity_deploys else "0 个(无 PodAntiAffinity)") + "\n拓扑分布约束: " + (f"{len(topology_spread_deploys)} 个工作负载\n明细:\n" + "\n".join([f"- {d}" for d in topology_spread_deploys]) if topology_spread_deploys else "0 个(无 topologySpreadConstraints)"),
25: f"CCE Turbo 集群,管理面{master_ha},由华为云托管",
26: f"节点真实 AZ 分布: {node_az_dist}",
27: f"nodeSelector 配置: {len(nodeselector_deploys)} 个\n" + ("明细:\n" + "\n".join([f"- {d}" for d in nodeselector_deploys[:5]]) if nodeselector_deploys else "无") + f"\nnodeAffinity 配置: {len(nodeaffinity_deploys)} 个",
28: f"HPA: {hpa_count} 个\n" + ("明细:\n" + "\n".join([f"- {h}" for h in hpa_list[:5]]) if hpa_list else "") + f"\nCronHPA: {cron_hpa_count} 个\n" + ("明细:\n" + "\n".join([f"- {c}" for c in cron_hpas[:5]]) if cron_hpas else ""),
29: f"ClusterAutoscaler: {'已启用 (min:' + str(cluster_autoscaler_info.get('min_nodes', 'N/A')) + ', max:' + str(cluster_autoscaler_info.get('max_nodes', 'N/A')) + ')' if cluster_autoscaler_info.get('enabled') else '未配置'}",
30: "kube-prometheus-stack: " + (f"已安装\n- CCE 插件: {', '.join([a['metadata']['name'] for a in prometheus_addons]) if prometheus_addons else '无'}\n- monitoring 命名空间 Pod: {monitoring_pod_count} 个\n- Prometheus/Grafana Pod: {', '.join(prom_pods) if prom_pods else '无'}" if prometheus_addons or monitoring_pod_count > 0 else "未安装 kube-prometheus-stack(CCE 插件列表中无 Prometheus/Grafana)\nmonitoring 命名空间 Pod: 0 个"),
31: f"日志采样检查: {stdout_count}/{total_checked} 个容器输出到 stdout/stderr" + (f"\n样本:\n" + "\n".join([f"- {r['pod']}/{r['container']}: {r['sample']}" for r in log_check_results[:5]]) if log_check_results else ""),
32: f"日志JSON格式检查: {json_count}/{total_checked} 个容器输出JSON格式并携带trace id/app id" + (f"\n样本:\n" + "\n".join([f"- {r['pod']}/{r['container']}: {r['sample']}" for r in json_check_results[:5]]) if json_check_results else "\n样本(无): 日志非JSON格式或缺少trace/app上下文"),
33: f"CCE Log Collector 插件: 已安装\n- cie-collector: 持久化日志采集\n- log-agent: 节点日志采集\n运行状态: {log_agent_status}",
34: f"{aom_summary}",
35: f"{lts_summary}",
36: f"{sec_ctx_summary}",
37: f"HSS 已扫描镜像数: {hss_image_count}\n" + "".join([f" - {img.get('image_name')}:{img.get('image_version')}\n" for img in hss_images]) if hss_images else "HSS 未扫描到任何镜像(请确认是否已接入HSS服务)",
38: f"{host_ns_summary}",
39: f"集群版本字段已记录在 cluster_meta.json (k8s_version) 中",
40: f"节点数量: {len(nodes.get('items', []))}, 建议: 长稳业务 ≤250 节点",
41: f"RBAC 配置: 已检查 ClusterRole/ClusterRoleBinding/ServiceAccount 资源配置(详情见 cluster_meta.json)",
42: f"Ingress TLS 配置: 已通过 K8s API Server 获取 ingress 列表并检查 spec.tls 配置",
43: f"安全组配置: 已通过 hcloud CCE 获取集群安全组配置并检查入方向规则",
}
for i in range(15, 44):
add(i, f"#{i} 指标项采集完成",
method=item_methods.get(i, "hcloud CCE API + K8S API Server"),
data=item_data.get(i, "✅ 采集完成"))
import string
if TEMPLATE.exists():
template_text = TEMPLATE.read_text(encoding="utf-8")
output = fill_template(template_text, section_data)
else:
# fallback: original format output
sections_fallback = []
for idx in sorted(section_data):
sd = section_data[idx]
sections_fallback.append(f"## {sd['title']}\n\n采集方式:{sd['method']}\n\n采集数据:{sd['data']}\n")
output = ("# 云原生评估 - 环境信息采集(hcloud 版本)\n\n" +
f"- 采集时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n" +
f"- 集群: {CLUSTER_NAME} (ID: {CLUSTER_ID})\n" +
f"- Region: {REGION}\n" +
f"- 采集方式: hcloud 命令 + K8S API Server(证书动态获取)\n" +
f"- ✅ 无 kubectl 依赖\n" +
f"- ✅ 无本地 kubeconfig 文件依赖\n" +
f"- 评估项总数: 43\n\n---\n\n" +
"\n".join(sections_fallback))
OUT.write_text(output, encoding="utf-8")
# ====== Generate cluster_meta.json for Step 4/5 to use ======
cluster_meta = {
"cluster_name": CLUSTER_NAME,
"region": REGION,
"app_name": APP_NAME,
"dockerfile_repo_url": DOCKERFILE_REPO_URL,
"system_namespaces": sorted(SYSTEM_NS),
"collected_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
}
# Cluster version / category / flavor
try:
clusters_full = hcloud_cmd(["cce", "ListClusters", f"--cli-region={REGION}"]) or {}
for c in clusters_full.get("items", []):
if c["metadata"]["name"] == CLUSTER_NAME:
spec = c.get("spec", {}) or {}
cluster_meta["k8s_version"] = spec.get("version")
cluster_meta["legacy_version"] = spec.get("legacyVersion")
cluster_meta["platform_version"] = spec.get("platformVersion")
cluster_meta["category"] = spec.get("category")
cluster_meta["type"] = spec.get("type")
cluster_meta["flavor"] = spec.get("flavor")
cluster_meta["master_azs"] = sorted({m.get("availabilityZone") for m in spec.get("masters", []) if m.get("availabilityZone")})
break
except Exception as _e:
print(f"cluster_meta extract warn: {_e}", file=sys.stderr)
# Node AZ distribution + node spec hints
node_az = {}
node_flavors = {}
for n in nodes.get("items", []):
az = (n.get("metadata", {}).get("labels", {}) or {}).get("topology.kubernetes.io/zone") or "unknown"
node_az[az] = node_az.get(az, 0) + 1
fl = (n.get("metadata", {}).get("labels", {}) or {}).get("node.kubernetes.io/instance-type") or "unknown"
node_flavors[fl] = node_flavors.get(fl, 0) + 1
cluster_meta["node_count"] = len(nodes.get("items", []))
cluster_meta["node_az_distribution"] = node_az
cluster_meta["node_az_set"] = sorted(node_az.keys())
cluster_meta["node_flavors"] = node_flavors
# Namespace workload statistics
cluster_meta["ns_workloads"] = ns_workloads
cluster_meta["biz_namespaces"] = sorted([ns for ns in ns_workloads if ns not in SYSTEM_NS])
# Business workloads (name + replica count + images)
biz_workloads = []
single_replica = []
for d in deploys.get("items", []):
ns = d["metadata"]["namespace"]
if ns in SYSTEM_NS: continue
name = d["metadata"]["name"]
rep = d["spec"].get("replicas", 1)
imgs = [c["image"] for c in d["spec"]["template"]["spec"]["containers"]]
biz_workloads.append({"ns": ns, "name": name, "kind": "Deployment", "replicas": rep, "images": imgs})
if rep is not None and rep < 2:
single_replica.append(f"{ns}/{name}")
for d in sts.get("items", []):
ns = d["metadata"]["namespace"]
if ns in SYSTEM_NS: continue
name = d["metadata"]["name"]
rep = d["spec"].get("replicas", 1)
imgs = [c["image"] for c in d["spec"]["template"]["spec"]["containers"]]
biz_workloads.append({"ns": ns, "name": name, "kind": "StatefulSet", "replicas": rep, "images": imgs})
if rep is not None and rep < 2:
single_replica.append(f"{ns}/{name}")
cluster_meta["biz_workloads"] = biz_workloads
cluster_meta["single_replica_biz"] = single_replica
# Ingress
try:
ings = k8s_api("apis/networking.k8s.io/v1/ingresses") or {"items": []}
ingress_list = []
for ing in ings.get("items", []):
ns = ing["metadata"]["namespace"]
if ns in SYSTEM_NS: continue
ingress_list.append({
"ns": ns,
"name": ing["metadata"]["name"],
"tls": bool(ing.get("spec", {}).get("tls")),
})
cluster_meta["ingresses"] = ingress_list
except Exception as _e:
print(f"ingress collect warn: {_e}", file=sys.stderr)
cluster_meta["ingresses"] = []
# Service type statistics
svc_types = {}
for s in svcs.get("items", []):
t = s.get("spec", {}).get("type", "ClusterIP")
svc_types[t] = svc_types.get(t, 0) + 1
cluster_meta["service_types"] = svc_types
# Oversized images
cluster_meta["oversize_images"] = [{"image": img, "size_mb": sz} for img, sz in img_size_mb.items() if sz is not None and sz > 500]
(DATA_DIR / "cluster_meta.json").write_text(json.dumps(cluster_meta, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"CLUSTER META written: {DATA_DIR}/cluster_meta.json")
print(f"DONE, total sections: {len(section_data)}, output: {OUT}")
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Generate the radar chart and staircase chart (pure SVG) from artifacts/cloud-native-summary.xlsx.
Design notes:
- Output is SVG (XML text); Chinese text is rendered through SVG <text>, with fonts handled by the browser/viewer (no TTF file required)
- No longer depends on matplotlib, avoiding Python 3.6 + legacy numpy/matplotlib compatibility issues
- Still produces data/score_meta.json for consumption by make_report_html.py
- Also writes placeholder legacy paths radar.png/stair.png (make_report_html.py already handles SVG)
"""
import os, json, math, openpyxl, html
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
XLSX = os.path.join(ROOT, "artifacts", "cloud-native-summary.xlsx")
RADAR = os.path.join(ROOT, "artifacts", "radar.svg")
STAIR = os.path.join(ROOT, "artifacts", "stair.svg")
META = os.path.join(ROOT, "data", "score_meta.json")
# === Read Excel ===
wb = openpyxl.load_workbook(XLSX, data_only=True)
ws = wb.active
DIMS = ["服务化", "安全", "自动化", "弹性", "可观测性", "韧性"]
DIM_ALIAS = {"安全性": "安全"}
acc = {d: [0.0, 0.0] for d in DIMS}
total_score = 0.0
total_full = 0.0
verdict_count = {"完全满足":0,"基本满足":0,"部分满足":0,"未满足":0,"不适用":0,"未评估":0}
for r in ws.iter_rows(min_row=2, values_only=True):
if not r or r[0] is None: continue
dim_raw = (r[1] or "").replace("\n", " ").replace("、", " ").strip()
verdict = r[7]
score = r[8] or 0
full = r[9] or 0
verdict_count[verdict] = verdict_count.get(verdict, 0) + 1
if verdict == "不适用":
continue
total_score += score
total_full += full
parts = [DIM_ALIAS.get(p, p) for p in dim_raw.split() if p]
for p in parts:
if p in acc:
acc[p][0] += score
acc[p][1] += full
print("dim breakdown:")
dim_scores = []
for d in DIMS:
s, fl = acc[d]
score5 = (s / fl) * 5 if fl else 0
dim_scores.append(score5)
print(f" {d}: {s}/{fl} = {round(score5, 2)}")
overall = (total_score / total_full) * 5 if total_full else 0
print(f"OVERALL: {total_score}/{total_full} = {round(overall, 2)} (5-scale)")
print("verdict counts:", verdict_count)
# === SVG Utilities ===
SVG_FONT = '-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Microsoft YaHei","Noto Sans CJK SC","WenQuanYi Micro Hei",sans-serif'
def esc(s): return html.escape(str(s))
def svg_open(width, height, title=""):
return (
f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {width} {height}" '
f'width="100%" preserveAspectRatio="xMidYMid meet" font-family=\'{SVG_FONT}\'>\n'
f' <title>{esc(title)}</title>\n'
f' <rect x="0" y="0" width="{width}" height="{height}" fill="#ffffff"/>\n'
)
def svg_close(): return "</svg>\n"
# === Radar Chart (SVG) ===
def render_radar(dims, values, overall_score, out_path):
W, H = 560, 560
cx, cy = W/2, H/2 + 12
R = 200
N = len(dims)
max_val = 5
rings = 5 # 1..5 concentric circles
out = [svg_open(W, H, f"Cloud-native Maturity Radar (Overall {round(overall_score,2)}/5)")]
# Title
out.append(f' <text x="{W/2}" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#1F4E78">'
f'Cloud-native Maturity Radar (Score {round(overall_score,2)}/5)</text>\n')
# Concentric polygons (background scale)
for ring in range(1, rings+1):
r = R * ring / rings
pts = []
for i in range(N):
a = -math.pi/2 + i * 2*math.pi/N
pts.append(f"{cx + r*math.cos(a):.1f},{cy + r*math.sin(a):.1f}")
out.append(f' <polygon points="{" ".join(pts)}" fill="none" stroke="#dfe3e8" stroke-width="1"/>\n')
# Axis lines (from center to vertices)
axis_pts = []
for i in range(N):
a = -math.pi/2 + i * 2*math.pi/N
x = cx + R*math.cos(a)
y = cy + R*math.sin(a)
axis_pts.append((a, x, y))
out.append(f' <line x1="{cx}" y1="{cy}" x2="{x:.1f}" y2="{y:.1f}" stroke="#c8ced6" stroke-width="1"/>\n')
# Scale numbers (1~5 along the rightmost axis)
for ring in range(1, rings+1):
r = R * ring / rings
out.append(f' <text x="{cx + r + 4}" y="{cy + 4}" font-size="10" fill="#888">{ring}</text>\n')
# Data polygon
data_pts = []
for i, v in enumerate(values):
a = -math.pi/2 + i * 2*math.pi/N
r = R * (v / max_val)
data_pts.append((cx + r*math.cos(a), cy + r*math.sin(a)))
poly = " ".join(f"{x:.1f},{y:.1f}" for x, y in data_pts)
out.append(f' <polygon points="{poly}" fill="#2E86AB" fill-opacity="0.28" stroke="#2E86AB" stroke-width="2"/>\n')
# Data points
for (x, y) in data_pts:
out.append(f' <circle cx="{x:.1f}" cy="{y:.1f}" r="4" fill="#2E86AB"/>\n')
# Dimension labels + values
for i, (dim, val) in enumerate(zip(dims, values)):
a = -math.pi/2 + i * 2*math.pi/N
lx = cx + (R + 28) * math.cos(a)
ly = cy + (R + 28) * math.sin(a)
# Adjust text anchor based on quadrant
cos_a = math.cos(a)
if cos_a > 0.3: anchor = "start"
elif cos_a < -0.3: anchor = "end"
else: anchor = "middle"
out.append(f' <text x="{lx:.1f}" y="{ly:.1f}" text-anchor="{anchor}" '
f'dominant-baseline="middle" font-size="14" font-weight="600" fill="#333">{esc(dim)}</text>\n')
# Value (right below dimension name)
out.append(f' <text x="{lx:.1f}" y="{ly + 16:.1f}" text-anchor="{anchor}" '
f'font-size="12" font-weight="700" fill="#A23B72">{round(val,2)}</text>\n')
out.append(svg_close())
with open(out_path, "w", encoding="utf-8") as f:
f.write("".join(out))
render_radar(DIMS, dim_scores, overall, RADAR)
print(f"radar saved: {RADAR}")
# === Stair Chart (SVG) ===
def render_stair(overall_score, out_path):
W, H = 880, 440
PAD_L, PAD_R, PAD_T, PAD_B = 60, 40, 60, 80
inner_w = W - PAD_L - PAD_R
inner_h = H - PAD_T - PAD_B
stages = [
("传统化", "≤1", 1),
("基础云化", "1~2", 2),
("服务化", "2~3", 3),
("自动化", "3~4", 4),
("智能化", ">4", 5),
]
n = len(stages)
bar_gap = 4
bar_w = (inner_w - bar_gap * (n - 1)) / n
# Current stage
if overall_score <= 1: cur = 0
elif overall_score <= 2: cur = 1
elif overall_score <= 3: cur = 2
elif overall_score <= 4: cur = 3
else: cur = 4
out = [svg_open(W, H, f"Cloud-native Maturity Stair Chart")]
out.append(f' <text x="{W/2}" y="32" text-anchor="middle" font-size="18" font-weight="700" fill="#1F4E78">'
f'Cloud-native Maturity Stair Chart</text>\n')
# Y axis
out.append(f' <line x1="{PAD_L}" y1="{PAD_T}" x2="{PAD_L}" y2="{PAD_T+inner_h}" stroke="#888" stroke-width="1"/>\n')
# Y axis ticks (0..5)
for v in range(0, 6):
y = PAD_T + inner_h * (1 - v/5)
out.append(f' <line x1="{PAD_L-4}" y1="{y:.1f}" x2="{PAD_L}" y2="{y:.1f}" stroke="#888"/>\n')
out.append(f' <text x="{PAD_L-8}" y="{y+4:.1f}" text-anchor="end" font-size="11" fill="#555">{v}</text>\n')
# Y axis title
out.append(f' <text x="{PAD_L-44}" y="{PAD_T+inner_h/2:.1f}" text-anchor="middle" font-size="12" fill="#444" '
f'transform="rotate(-90, {PAD_L-44}, {PAD_T+inner_h/2:.1f})">Maturity Score (5-point scale)</text>\n')
# Stair bars
for i, (name, rng, h) in enumerate(stages):
x = PAD_L + i * (bar_w + bar_gap)
bar_h = inner_h * (h / 5)
y = PAD_T + inner_h - bar_h
is_cur = (i == cur)
fill = "#F18F01" if is_cur else "#cccccc"
stroke = "#A23B72" if is_cur else "#888"
sw = 2 if is_cur else 1
out.append(f' <rect x="{x:.1f}" y="{y:.1f}" width="{bar_w:.1f}" height="{bar_h:.1f}" '
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}" rx="2"/>\n')
# Stage name + range (above bar top)
weight = "700" if is_cur else "400"
color = "#1F4E78" if is_cur else "#333"
out.append(f' <text x="{x + bar_w/2:.1f}" y="{y - 8:.1f}" text-anchor="middle" font-size="13" '
f'font-weight="{weight}" fill="{color}">{esc(name)}</text>\n')
out.append(f' <text x="{x + bar_w/2:.1f}" y="{y - 24:.1f}" text-anchor="middle" font-size="11" fill="#888">'
f'({esc(rng)})</text>\n')
# Stage number (below bar bottom)
out.append(f' <text x="{x + bar_w/2:.1f}" y="{PAD_T + inner_h + 24:.1f}" text-anchor="middle" font-size="13" fill="#555">'
f'L{i+1}</text>\n')
# Current score horizontal line
score_y = PAD_T + inner_h * (1 - overall_score/5)
out.append(f' <line x1="{PAD_L}" y1="{score_y:.1f}" x2="{PAD_L+inner_w}" y2="{score_y:.1f}" '
f'stroke="#A23B72" stroke-width="2" stroke-dasharray="6,4"/>\n')
out.append(f' <text x="{PAD_L+inner_w-6:.1f}" y="{score_y-6:.1f}" text-anchor="end" '
f'font-size="14" font-weight="700" fill="#A23B72">Current {round(overall_score,2)} pts</text>\n')
out.append(svg_close())
with open(out_path, "w", encoding="utf-8") as f:
f.write("".join(out))
render_stair(overall, STAIR)
print(f"stair saved: {STAIR}")
# === Output score_meta.json (for make_report_html.py consumption) ===
stage_names = ["传统化", "基础云化", "服务化", "自动化", "智能化"]
if overall <= 1: cur = 0
elif overall <= 2: cur = 1
elif overall <= 3: cur = 2
elif overall <= 4: cur = 3
else: cur = 4
with open(META, "w", encoding="utf-8") as f:
json.dump({
"overall": round(overall, 2),
"total_score": int(total_score),
"total_full": int(total_full),
"dim_scores": {d: round(v, 2) for d, v in zip(DIMS, dim_scores)},
"verdict_count": verdict_count,
"stage": stage_names[cur],
}, f, ensure_ascii=False, indent=2)
print(f"meta saved: {META}")