diff --git a/ansible/gather-host-facts.yml b/ansible/gather-host-facts.yml index 60efc17..5b2d01a 100644 --- a/ansible/gather-host-facts.yml +++ b/ansible/gather-host-facts.yml @@ -3,7 +3,59 @@ hosts: "{{ target_hosts | default('all') }}" become: true gather_facts: true + vars: + delivery_lookup_path: "{{ lookup_path | default('') }}" tasks: + - name: Show directory completions + ansible.builtin.shell: | + set -euo pipefail + python3 - <<'PY' + import json + import os + import sys + + prefix = os.environ.get("LOOKUP_PATH", "").strip() + if not prefix.startswith("/") or "\x00" in prefix: + print("XINFRA_PATH_COMPLETIONS_JSON=[]") + sys.exit(0) + + if prefix.endswith("/"): + parent = prefix.rstrip("/") or "/" + needle = "" + else: + parent = os.path.dirname(prefix) or "/" + needle = os.path.basename(prefix) + + items = [] + try: + children = sorted(os.listdir(parent)) + except OSError: + children = [] + + for name in children: + if needle and not name.startswith(needle): + continue + path = os.path.join(parent, name) if parent != "/" else "/" + name + if not os.path.isdir(path): + continue + available_gi = 0 + try: + stat = os.statvfs(path) + available_gi = int(stat.f_bavail * stat.f_frsize / 1073741824) + except OSError: + pass + items.append({"path": path, "available_gi": available_gi}) + + print("XINFRA_PATH_COMPLETIONS_JSON=" + json.dumps(items, ensure_ascii=False)) + PY + args: + executable: /bin/bash + environment: + LOOKUP_PATH: "{{ delivery_lookup_path }}" + changed_when: false + when: delivery_lookup_path | length > 0 + - name: Show discovered mounts ansible.builtin.debug: var: ansible_mounts + when: delivery_lookup_path | length == 0 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 95b7471..8fd3208 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -12,6 +12,7 @@ declare module 'vue' { AppSidebar: typeof import('./src/components/Layout/AppSidebar.vue')['default'] AuditLogTable: typeof import('./src/components/AuditLogTable.vue')['default'] BusinessLineSwitcher: typeof import('./src/components/BusinessLineSwitcher.vue')['default'] + ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete'] ElButton: typeof import('element-plus/es')['ElButton'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDropdown: typeof import('element-plus/es')['ElDropdown'] @@ -26,7 +27,6 @@ declare module 'vue' { ElPagination: typeof import('element-plus/es')['ElPagination'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElSlider: typeof import('element-plus/es')['ElSlider'] - ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] RouterLink: typeof import('vue-router')['RouterLink'] diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4d25368..c22f458 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,6 @@ "version": "0.1.0", "dependencies": { "@element-plus/icons-vue": "^2.1.0", - "@tanstack/vue-virtual": "^3.13.34", "axios": "^1.5.0", "element-plus": "^2.3.12", "pinia": "^2.1.4", @@ -994,32 +993,6 @@ "win32" ] }, - "node_modules/@tanstack/virtual-core": { - "version": "3.17.6", - "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.6.tgz", - "integrity": "sha512-h0/Ebo18CkOrChlQIhNtQkM5ySUnh/GumQ/D1st3hG2HWUPEF+ILUc2k29UtivCi/9G7w7G3/f7Xyd5cCFbKBw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, - "node_modules/@tanstack/vue-virtual": { - "version": "3.13.34", - "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.34.tgz", - "integrity": "sha512-CBqbCcnVsKpl9IJ7frPnbBmAqmc7JttSySg04kgLYJ4yYuC8JsAbtUHP0yLtWGLyByjihN3SwaDCgHQ+Z4iPoA==", - "license": "MIT", - "dependencies": { - "@tanstack/virtual-core": "3.17.6" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "vue": "^2.7.0 || ^3.0.0" - } - }, "node_modules/@types/estree": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9acc643..c0666a2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,6 @@ }, "dependencies": { "@element-plus/icons-vue": "^2.1.0", - "@tanstack/vue-virtual": "^3.13.34", "axios": "^1.5.0", "element-plus": "^2.3.12", "pinia": "^2.1.4", diff --git a/frontend/src/api/businessLine.ts b/frontend/src/api/businessLine.ts index 5de8442..1cbd27e 100644 --- a/frontend/src/api/businessLine.ts +++ b/frontend/src/api/businessLine.ts @@ -14,6 +14,11 @@ export interface WayneNamespace { kubeNamespace: string } +export interface SinaOrganization { + id: string + name: string +} + export const businessLineApi = { async listMine(): Promise { const token = getToken() @@ -87,6 +92,28 @@ export const businessLineApi = { body: JSON.stringify({ namespaces }), }) }, + + async listSinaOrganizations(businessLineId: number, keyword = ''): Promise { + const params = new URLSearchParams() + if (keyword.trim()) { + params.set('keyword', keyword.trim()) + } + const suffix = params.toString() ? `?${params.toString()}` : '' + const data = await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organizations${suffix}`) + return Array.isArray(data.items) ? data.items : [] + }, + + async listMappedSinaOrganizations(businessLineId: number): Promise { + const data = await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`) + return Array.isArray(data.items) ? data.items : [] + }, + + async replaceMappedSinaOrganizations(businessLineId: number, organizations: SinaOrganization[]): Promise { + await request(`/auth/api/v1/business-lines/${businessLineId}/sina-organization-mappings`, { + method: 'PUT', + body: JSON.stringify({ organizations }), + }) + }, } async function request(path: string, init: RequestInit = {}) { diff --git a/frontend/src/api/delivery.ts b/frontend/src/api/delivery.ts index 1e4458a..fd8c1dd 100644 --- a/frontend/src/api/delivery.ts +++ b/frontend/src/api/delivery.ts @@ -66,6 +66,7 @@ export interface DeliveryTask { updated_at: string started_at?: string finished_at?: string + credential_available?: boolean } export interface TaskEvent { @@ -85,8 +86,12 @@ export interface DeliveryTaskSnapshot { } export interface DeploymentCredential { - username: string + service: string + instance_name: string host: string + port: number + username: string + account_host: string password: string } @@ -109,8 +114,13 @@ export const deliveryApi = { return Array.isArray(data.items) ? data.items : [] }, - async listTargetMountPaths(targetId: number, host: string): Promise { - const data = await authRequest(`/auth/api/v1/delivery/targets/${targetId}/hosts/${encodeURIComponent(host)}/mount-paths`) + async listTargetMountPaths(targetId: number, host: string, prefix = ''): Promise { + const search = new URLSearchParams() + if (prefix) search.set('prefix', prefix) + const query = search.toString() + const data = await authRequest( + `/auth/api/v1/delivery/targets/${targetId}/hosts/${encodeURIComponent(host)}/mount-paths${query ? `?${query}` : ''}`, + ) return Array.isArray(data.items) ? data.items : [] }, diff --git a/frontend/src/api/machine.ts b/frontend/src/api/machine.ts index 4e8c237..53fe66a 100644 --- a/frontend/src/api/machine.ts +++ b/frontend/src/api/machine.ts @@ -46,13 +46,13 @@ export interface MachineResourceList { export interface MachineResourceQuery { page: number size: number + businessLineId?: number hostname?: string assetNumber?: string type?: string location?: string ip?: string spec?: string - businessLine?: string source?: string status?: string } @@ -83,21 +83,24 @@ export const emptyMachineOverview: MachineOverview = { } export const machineApi = { - async getOverview(): Promise { - return authRequest('/auth/api/v1/machines/overview') + async getOverview(businessLineId?: number): Promise { + const params = new URLSearchParams() + if (businessLineId) params.set('business_line_id', String(businessLineId)) + const suffix = params.toString() ? `?${params.toString()}` : '' + return authRequest(`/auth/api/v1/machines/overview${suffix}`) }, async listResources(query: MachineResourceQuery): Promise { const params = new URLSearchParams() params.set('page', String(query.page)) params.set('size', String(query.size)) + if (query.businessLineId) params.set('business_line_id', String(query.businessLineId)) if (query.hostname) params.set('hostname', query.hostname) if (query.assetNumber) params.set('assetNumber', query.assetNumber) if (query.type) params.set('type', query.type) if (query.location) params.set('location', query.location) if (query.ip) params.set('ip', query.ip) if (query.spec) params.set('spec', query.spec) - if (query.businessLine) params.set('businessLine', query.businessLine) if (query.source) params.set('source', query.source) if (query.status) params.set('status', query.status) const data = await authRequest(`/auth/api/v1/machines/resources?${params.toString()}`) diff --git a/frontend/src/views/businessLine/Assignment.vue b/frontend/src/views/businessLine/Assignment.vue index e66124a..79be692 100644 --- a/frontend/src/views/businessLine/Assignment.vue +++ b/frontend/src/views/businessLine/Assignment.vue @@ -45,6 +45,34 @@ 保存 Wayne namespace 映射 + +
+ + + + + + + + 保存 SINA 映射 +
需要当前业务线管理员权限
@@ -53,7 +81,7 @@ + + + ${tableRows}
+ +` + const url = URL.createObjectURL(new Blob([content], { type: 'application/vnd.ms-excel;charset=utf-8' })) const link = document.createElement('a') link.href = url - link.download = `${deliveryForm.instanceName}-root-credential.txt` + link.download = `${primary.instance_name || deliveryForm.instanceName}-credential.xls` link.click() URL.revokeObjectURL(url) - ElMessage.success('凭证文件已下载,请妥善保管') + ElMessage.success('Excel 凭证已下载,请妥善保管') } function loadDeliveryHistory(): DeliveryHistoryItem[] { @@ -2795,7 +2901,25 @@ h4 { gap: 10px; } -.credential-secret > code { +.credential-account { + display: grid; + gap: 8px; +} + +.credential-field { + display: grid; + grid-template-columns: 64px minmax(0, 1fr) 32px; + align-items: center; + gap: 8px; +} + +.credential-field > span { + color: var(--text-dim); + font-size: 11.5px; + font-weight: 700; +} + +.credential-field > code { overflow-wrap: anywhere; padding: 9px 10px; border: 1px solid var(--line-soft); @@ -2805,6 +2929,11 @@ h4 { font-size: 12px; } +.credential-field .el-button { + width: 32px; + min-width: 32px; +} + .credential-actions { display: flex; flex-wrap: wrap; diff --git a/frontend/src/views/service/Management.vue b/frontend/src/views/service/Management.vue index bcdee5d..9da20de 100644 --- a/frontend/src/views/service/Management.vue +++ b/frontend/src/views/service/Management.vue @@ -223,7 +223,7 @@

凭证仅可领取一次,领取后服务端立即销毁明文。

领取一次性凭证 -

该任务的一次性凭证已领取或不可用,平台不再提供明文密码。

+

该任务的一次性凭证已领取或不可用,平台不再提供明文密码。

仅交付成功的任务提供一次性凭证。

@@ -266,7 +266,6 @@ const deliveryDetailVisible = ref(false) const selectedDeliveryTask = ref() const revealedCredentials = ref([]) const credentialRevealing = ref(false) -const consumedCredentialTasks = ref>(new Set()) const containerServices = ref([]) const containerSummary = ref({ ...emptyContainerServiceSummary }) @@ -354,32 +353,40 @@ function deliveryTasksForService(serviceName: string) { } function credentialEligible(task: DeliveryTask) { - return ['finished', 'register_failed'].includes(task.status) && !consumedCredentialTasks.value.has(task.id) + return ['finished', 'register_failed'].includes(task.status) && Boolean(task.credential_available) } function credentialStatusText(task: DeliveryTask) { - if (consumedCredentialTasks.value.has(task.id)) return '已领取' - return credentialEligible(task) ? '可领取' : '不提供' + if (credentialEligible(task)) return '可领取' + if (['finished', 'register_failed'].includes(task.status)) return '已领取' + return '不提供' } function credentialStatusClass(task: DeliveryTask) { - if (consumedCredentialTasks.value.has(task.id)) return 'tag-green' return credentialEligible(task) ? 'tag-amber' : '' } +function markCredentialUnavailable(taskID: string) { + const task = deliveryRecords.value.find((item) => item.id === taskID) + if (task) task.credential_available = false + if (selectedDeliveryTask.value?.id === taskID) { + selectedDeliveryTask.value.credential_available = false + } +} + async function revealDeliveryCredential(task: DeliveryTask) { credentialRevealing.value = true try { const items = await deliveryApi.revealCredentials(task.id) if (items.length === 0) { - consumedCredentialTasks.value.add(task.id) + markCredentialUnavailable(task.id) ElMessage.warning('该任务没有可领取的凭证') return } revealedCredentials.value = items - consumedCredentialTasks.value.add(task.id) + markCredentialUnavailable(task.id) } catch (error) { - consumedCredentialTasks.value.add(task.id) + markCredentialUnavailable(task.id) ElMessage.error(error instanceof Error ? error.message : '凭证领取失败') } finally { credentialRevealing.value = false diff --git a/frontend/src/views/task/TaskCenter.vue b/frontend/src/views/task/TaskCenter.vue index c68f673..93f2fdb 100644 --- a/frontend/src/views/task/TaskCenter.vue +++ b/frontend/src/views/task/TaskCenter.vue @@ -42,11 +42,7 @@ 暂无任务记录 @@ -56,29 +52,11 @@

任务日志 {{ selectedTaskName }} - ● 实时更新中 - 重试中 ({{ streamRetryCount }}/3)

-
- - {{ selectedTaskMeta }} -
+ {{ selectedTaskMeta }} -
- -
+
+
{{ log.time }}{{ log.message }}
@@ -94,8 +72,8 @@