feat(lite): XOA quick deploy (#7245)

This commit is contained in:
Pierre Donias 2023-12-22 15:58:54 +01:00 committed by GitHub
parent cc080ec681
commit b0e000328d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 849 additions and 3 deletions

View File

@ -7,6 +7,7 @@
- Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223)) - Added tooltip on CPU provisioning warning icon (PR [#7223](https://github.com/vatesfr/xen-orchestra/pull/7223))
- Add indeterminate state on FormToggle component (PR [#7230](https://github.com/vatesfr/xen-orchestra/pull/7230)) - Add indeterminate state on FormToggle component (PR [#7230](https://github.com/vatesfr/xen-orchestra/pull/7230))
- Add new UiStatusPanel component (PR [#7227](https://github.com/vatesfr/xen-orchestra/pull/7227)) - Add new UiStatusPanel component (PR [#7227](https://github.com/vatesfr/xen-orchestra/pull/7227))
- XOA quick deploy (PR [#7245](https://github.com/vatesfr/xen-orchestra/pull/7245))
- Fix infinite loader when no stats on pool dashboard (PR [#7236](https://github.com/vatesfr/xen-orchestra/pull/7236)) - Fix infinite loader when no stats on pool dashboard (PR [#7236](https://github.com/vatesfr/xen-orchestra/pull/7236))
## **0.1.6** (2023-11-30) ## **0.1.6** (2023-11-30)

View File

@ -21,7 +21,8 @@ a {
} }
code, code,
code * { code *,
pre {
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace; "Courier New", monospace;
} }

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 43 KiB

View File

@ -13,6 +13,9 @@
<slot /> <slot />
<div class="right"> <div class="right">
<PoolOverrideWarning as-tooltip /> <PoolOverrideWarning as-tooltip />
<UiButton v-if="isDesktop" :icon="faDownload" @click="openXoaDeploy">
{{ $t("deploy-xoa") }}
</UiButton>
<AccountButton /> <AccountButton />
</div> </div>
</header> </header>
@ -22,14 +25,20 @@
import AccountButton from "@/components/AccountButton.vue"; import AccountButton from "@/components/AccountButton.vue";
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue"; import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
import TextLogo from "@/components/TextLogo.vue"; import TextLogo from "@/components/TextLogo.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue"; import UiIcon from "@/components/ui/icon/UiIcon.vue";
import { useNavigationStore } from "@/stores/navigation.store"; import { useNavigationStore } from "@/stores/navigation.store";
import { useRouter } from "vue-router";
import { useUiStore } from "@/stores/ui.store"; import { useUiStore } from "@/stores/ui.store";
import { faBars } from "@fortawesome/free-solid-svg-icons"; import { faBars, faDownload } from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
const router = useRouter();
const openXoaDeploy = () => router.push({ name: "xoa.deploy" });
const uiStore = useUiStore(); const uiStore = useUiStore();
const { isMobile } = storeToRefs(uiStore); const { isMobile, isDesktop } = storeToRefs(uiStore);
const navigationStore = useNavigationStore(); const navigationStore = useNavigationStore();
const { trigger: navigationTrigger } = storeToRefs(navigationStore); const { trigger: navigationTrigger } = storeToRefs(navigationStore);
@ -62,5 +71,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
.right { .right {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 2rem;
} }
</style> </style>

View File

@ -0,0 +1,34 @@
<template>
<UiModal color="error" @submit="modal.approve()">
<ConfirmModalLayout :icon="faExclamationCircle">
<template #title>{{ $t("invalid-field") }}</template>
<template #default>
{{ message }}
</template>
<template #buttons>
<ModalApproveButton>
{{ $t("ok") }}
</ModalApproveButton>
</template>
</ConfirmModalLayout>
</UiModal>
</template>
<script lang="ts" setup>
import ConfirmModalLayout from "@/components/ui/modals/layouts/ConfirmModalLayout.vue";
import ModalApproveButton from "@/components/ui/modals/ModalApproveButton.vue";
import UiModal from "@/components/ui/modals/UiModal.vue";
import { IK_MODAL } from "@/types/injection-keys";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons";
import { inject } from "vue";
defineProps<{
message: string;
}>();
const modal = inject(IK_MODAL)!;
</script>
<style lang="postcss" scoped></style>

View File

@ -0,0 +1,18 @@
<template>
<pre class="ui-raw"><slot /></pre>
</template>
<script lang="ts" setup></script>
<style lang="postcss" scoped>
.ui-raw {
background-color: var(--color-blue-scale-400);
text-align: left;
overflow: auto;
max-width: 100%;
width: 48em;
padding: 0.5em;
border-radius: 8px;
line-height: 150%;
}
</style>

View File

@ -54,6 +54,7 @@ type ObjectTypeToRecordMapping = {
host: XenApiHost; host: XenApiHost;
host_metrics: XenApiHostMetrics; host_metrics: XenApiHostMetrics;
message: XenApiMessage<any>; message: XenApiMessage<any>;
network: XenApiNetwork;
pool: XenApiPool; pool: XenApiPool;
sr: XenApiSr; sr: XenApiSr;
vm: XenApiVm; vm: XenApiVm;
@ -113,9 +114,11 @@ export interface XenApiHost extends XenApiRecord<"host"> {
} }
export interface XenApiSr extends XenApiRecord<"sr"> { export interface XenApiSr extends XenApiRecord<"sr"> {
content_type: string;
name_label: string; name_label: string;
physical_size: number; physical_size: number;
physical_utilisation: number; physical_utilisation: number;
shared: boolean;
} }
export interface XenApiVm extends XenApiRecord<"vm"> { export interface XenApiVm extends XenApiRecord<"vm"> {

View File

@ -1,9 +1,13 @@
{ {
"about": "About", "about": "About",
"access-xoa": "Access XOA",
"add": "Add", "add": "Add",
"add-filter": "Add filter", "add-filter": "Add filter",
"add-or": "+OR", "add-or": "+OR",
"add-sort": "Add sort", "add-sort": "Add sort",
"admin-login": "Admin login",
"admin-password": "Admin password",
"admin-password-confirm": "Confirm admin password",
"alarm-type": { "alarm-type": {
"cpu_usage": "CPU usage exceeds {n}%", "cpu_usage": "CPU usage exceeds {n}%",
"disk_usage": "Disk usage exceeds {n}%", "disk_usage": "Disk usage exceeds {n}%",
@ -26,12 +30,14 @@
"backup": "Backup", "backup": "Backup",
"cancel": "Cancel", "cancel": "Cancel",
"change-state": "Change state", "change-state": "Change state",
"check-errors": "Check out the errors:",
"click-to-display-alarms": "Click to display alarms:", "click-to-display-alarms": "Click to display alarms:",
"click-to-return-default-pool": "Click here to return to the default pool", "click-to-return-default-pool": "Click here to return to the default pool",
"close": "Close", "close": "Close",
"coming-soon": "Coming soon!", "coming-soon": "Coming soon!",
"community": "Community", "community": "Community",
"community-name": "{name} community", "community-name": "{name} community",
"configuration": "Configuration",
"confirm-cancel": "Are you sure you want to cancel?", "confirm-cancel": "Are you sure you want to cancel?",
"confirm-delete": "You're about to delete {0}", "confirm-delete": "You're about to delete {0}",
"console": "Console", "console": "Console",
@ -43,14 +49,28 @@
"dashboard": "Dashboard", "dashboard": "Dashboard",
"delete": "Delete", "delete": "Delete",
"delete-vms": "Delete 1 VM | Delete {n} VMs", "delete-vms": "Delete 1 VM | Delete {n} VMs",
"deploy": "Deploy",
"deploy-xoa": "Deploy XOA",
"deploy-xoa-available-on-desktop": "XOA deployment is available on your desktop interface",
"deploy-xoa-status": {
"configuring": "Configuring XOA…",
"importing": "Importing XOA…",
"not-responding": "XOA is not responding",
"ready": "XOA is ready!",
"starting": "Starting XOA…",
"waiting": "Waiting for XOA to respond…"
},
"descending": "descending", "descending": "descending",
"description": "Description", "description": "Description",
"dhcp": "DHCP",
"disabled": "Disabled", "disabled": "Disabled",
"display": "Display", "display": "Display",
"dns": "DNS",
"do-you-have-needs": "You have needs and/or expectations? Let us know", "do-you-have-needs": "You have needs and/or expectations? Let us know",
"documentation": "Documentation", "documentation": "Documentation",
"documentation-name": "{name} documentation", "documentation-name": "{name} documentation",
"edit-config": "Edit config", "edit-config": "Edit config",
"enabled": "Enabled",
"error-no-data": "Error, can't collect data.", "error-no-data": "Error, can't collect data.",
"error-occurred": "An error has occurred", "error-occurred": "An error has occurred",
"export": "Export", "export": "Export",
@ -84,11 +104,16 @@
"force-shutdown": "Force shutdown", "force-shutdown": "Force shutdown",
"fullscreen": "Fullscreen", "fullscreen": "Fullscreen",
"fullscreen-leave": "Leave fullscreen", "fullscreen-leave": "Leave fullscreen",
"gateway": "Gateway",
"n-gb-left": "{n} GB left",
"n-gb-required": "{n} GB required",
"go-back": "Go back", "go-back": "Go back",
"gzip": "gzip", "gzip": "gzip",
"here": "Here", "here": "Here",
"hosts": "Hosts", "hosts": "Hosts",
"invalid-field": "Invalid field",
"keep-me-logged": "Keep me logged in", "keep-me-logged": "Keep me logged in",
"keep-page-open": "Do not refresh or quit tab before end of deployment.",
"language": "Language", "language": "Language",
"last-week": "Last week", "last-week": "Last week",
"learn-more": "Learn more", "learn-more": "Learn more",
@ -104,6 +129,7 @@
"n-missing": "{n} missing", "n-missing": "{n} missing",
"n-vms": "1 VM | {n} VMs", "n-vms": "1 VM | {n} VMs",
"name": "Name", "name": "Name",
"netmask": "Netmask",
"network": "Network", "network": "Network",
"network-download": "Download", "network-download": "Download",
"network-throughput": "Network throughput", "network-throughput": "Network throughput",
@ -119,6 +145,7 @@
"not-found": "Not found", "not-found": "Not found",
"object": "Object", "object": "Object",
"object-not-found": "Object {id} can't be found…", "object-not-found": "Object {id} can't be found…",
"ok": "OK",
"on-object": "on {object}", "on-object": "on {object}",
"open-console-in-new-tab": "Open console in new tab", "open-console-in-new-tab": "Open console in new tab",
"or": "Or", "or": "Or",
@ -154,14 +181,23 @@
"selected-vms-in-execution": "Some selected VMs are running", "selected-vms-in-execution": "Some selected VMs are running",
"send-ctrl-alt-del": "Send Ctrl+Alt+Del", "send-ctrl-alt-del": "Send Ctrl+Alt+Del",
"send-us-feedback": "Send us feedback", "send-us-feedback": "Send us feedback",
"select": {
"network": "Select a network",
"storage": "Select a storage"
},
"settings": "Settings", "settings": "Settings",
"shutdown": "Shutdown", "shutdown": "Shutdown",
"snapshot": "Snapshot", "snapshot": "Snapshot",
"sort-by": "Sort by", "sort-by": "Sort by",
"ssh-account": "SSH account",
"ssh-login": "SSH login",
"ssh-password": "SSH password",
"ssh-password-confirm": "Confirm SSH password",
"stacked-cpu-usage": "Stacked CPU usage", "stacked-cpu-usage": "Stacked CPU usage",
"stacked-ram-usage": "Stacked RAM usage", "stacked-ram-usage": "Stacked RAM usage",
"start": "Start", "start": "Start",
"start-on-host": "Start on specific host", "start-on-host": "Start on specific host",
"static-ip": "Static IP",
"stats": "Stats", "stats": "Stats",
"status": "Status", "status": "Status",
"storage": "Storage", "storage": "Storage",
@ -193,6 +229,15 @@
"vm-is-running": "The VM is running", "vm-is-running": "The VM is running",
"vms": "VMs", "vms": "VMs",
"xo-lite-under-construction": "XOLite is under construction", "xo-lite-under-construction": "XOLite is under construction",
"xoa-admin-account": "XOA admin account",
"xoa-deploy": "XOA deployment",
"xoa-deploy-failed": "Sorry, deployment failed!",
"xoa-deploy-retry": "Try again to deploy XOA",
"xoa-deploy-successful": "XOA deployment successful!",
"xoa-ip": "XOA IP address",
"xoa-password-confirm-different": "XOA password confirmation is different",
"xoa-ssh-account": "XOA SSH account",
"xoa-ssh-password-confirm-different": "SSH password confirmation is different",
"you-are-currently-on": "You are currently on: {0}", "you-are-currently-on": "You are currently on: {0}",
"zstd": "zstd" "zstd": "zstd"
} }

View File

@ -1,9 +1,13 @@
{ {
"about": "À propos", "about": "À propos",
"access-xoa": "Accéder à la XOA",
"add": "Ajouter", "add": "Ajouter",
"add-filter": "Ajouter un filtre", "add-filter": "Ajouter un filtre",
"add-or": "+OU", "add-or": "+OU",
"add-sort": "Ajouter un tri", "add-sort": "Ajouter un tri",
"admin-login": "Nom d'utilisateur administrateur",
"admin-password": "Mot de passe administrateur",
"admin-password-confirm": "Confirmer le mot de passe administrateur",
"alarm-type": { "alarm-type": {
"cpu_usage": "L'utilisation du CPU dépasse {n}%", "cpu_usage": "L'utilisation du CPU dépasse {n}%",
"disk_usage": "L'utilisation du disque dépasse {n}%", "disk_usage": "L'utilisation du disque dépasse {n}%",
@ -26,12 +30,14 @@
"backup": "Sauvegarde", "backup": "Sauvegarde",
"cancel": "Annuler", "cancel": "Annuler",
"change-state": "Changer l'état", "change-state": "Changer l'état",
"check-errors": "Consultez les erreurs :",
"click-to-display-alarms": "Cliquer pour afficher les alarmes :", "click-to-display-alarms": "Cliquer pour afficher les alarmes :",
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut", "click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
"close": "Fermer", "close": "Fermer",
"coming-soon": "Bientôt disponible !", "coming-soon": "Bientôt disponible !",
"community": "Communauté", "community": "Communauté",
"community-name": "Communauté {name}", "community-name": "Communauté {name}",
"configuration": "Configuration",
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?", "confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
"confirm-delete": "Vous êtes sur le point de supprimer {0}", "confirm-delete": "Vous êtes sur le point de supprimer {0}",
"console": "Console", "console": "Console",
@ -43,14 +49,28 @@
"dashboard": "Tableau de bord", "dashboard": "Tableau de bord",
"delete": "Supprimer", "delete": "Supprimer",
"delete-vms": "Supprimer 1 VM | Supprimer {n} VMs", "delete-vms": "Supprimer 1 VM | Supprimer {n} VMs",
"deploy": "Déployer",
"deploy-xoa": "Déployer XOA",
"deploy-xoa-available-on-desktop": "Le déploiement de la XOA est disponible sur ordinateur",
"deploy-xoa-status": {
"configuring": "Configuration de la XOA…",
"importing": "Importation de la XOA…",
"not-responding": "La XOA ne répond pas",
"ready": "La XOA est prête !",
"starting": "Démarrage de la XOA…",
"waiting": "En attente de réponse de la XOA…"
},
"descending": "descendant", "descending": "descendant",
"description": "Description", "description": "Description",
"dhcp": "DHCP",
"dns": "DNS",
"disabled": "Désactivé", "disabled": "Désactivé",
"display": "Affichage", "display": "Affichage",
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir", "do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
"documentation": "Documentation", "documentation": "Documentation",
"documentation-name": "Documentation {name}", "documentation-name": "Documentation {name}",
"edit-config": "Modifier config", "edit-config": "Modifier config",
"enabled": "Activé",
"error-no-data": "Erreur, impossible de collecter les données.", "error-no-data": "Erreur, impossible de collecter les données.",
"error-occurred": "Une erreur est survenue", "error-occurred": "Une erreur est survenue",
"export": "Exporter", "export": "Exporter",
@ -84,11 +104,16 @@
"force-shutdown": "Forcer l'arrêt", "force-shutdown": "Forcer l'arrêt",
"fullscreen": "Plein écran", "fullscreen": "Plein écran",
"fullscreen-leave": "Quitter plein écran", "fullscreen-leave": "Quitter plein écran",
"gateway": "Passerelle",
"n-gb-left": "{n} Go libres",
"n-gb-required": "{n} Go requis",
"go-back": "Revenir en arrière", "go-back": "Revenir en arrière",
"gzip": "gzip", "gzip": "gzip",
"here": "Ici", "here": "Ici",
"hosts": "Hôtes", "hosts": "Hôtes",
"invalid-field": "Champ invalide",
"keep-me-logged": "Rester connecté", "keep-me-logged": "Rester connecté",
"keep-page-open": "Ne pas rafraichir ou quitter cette page avant la fin du déploiement.",
"language": "Langue", "language": "Langue",
"last-week": "Semaine dernière", "last-week": "Semaine dernière",
"learn-more": "En savoir plus", "learn-more": "En savoir plus",
@ -104,6 +129,7 @@
"n-missing": "{n} manquant | {n} manquants", "n-missing": "{n} manquant | {n} manquants",
"n-vms": "1 VM | {n} VMs", "n-vms": "1 VM | {n} VMs",
"name": "Nom", "name": "Nom",
"netmask": "Masque réseau",
"network": "Réseau", "network": "Réseau",
"network-download": "Descendant", "network-download": "Descendant",
"network-throughput": "Débit du réseau", "network-throughput": "Débit du réseau",
@ -119,6 +145,7 @@
"not-found": "Non trouvé", "not-found": "Non trouvé",
"object": "Objet", "object": "Objet",
"object-not-found": "L'objet {id} est introuvable…", "object-not-found": "L'objet {id} est introuvable…",
"ok": "OK",
"on-object": "sur {object}", "on-object": "sur {object}",
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet", "open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
"or": "Ou", "or": "Ou",
@ -154,14 +181,23 @@
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution", "selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr", "send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
"send-us-feedback": "Envoyez-nous vos commentaires", "send-us-feedback": "Envoyez-nous vos commentaires",
"select": {
"network": "Sélectionner un réseau",
"storage": "Sélectionner un SR"
},
"settings": "Paramètres", "settings": "Paramètres",
"shutdown": "Arrêter", "shutdown": "Arrêter",
"snapshot": "Instantané", "snapshot": "Instantané",
"sort-by": "Trier par", "sort-by": "Trier par",
"ssh-account": "Compte SSH",
"ssh-login": "Nom d'utilisateur SSH",
"ssh-password": "Mot de passe SSH",
"ssh-password-confirm": "Confirmer le mot de passe SSH",
"stacked-cpu-usage": "Utilisation CPU empilée", "stacked-cpu-usage": "Utilisation CPU empilée",
"stacked-ram-usage": "Utilisation RAM empilée", "stacked-ram-usage": "Utilisation RAM empilée",
"start": "Démarrer", "start": "Démarrer",
"start-on-host": "Démarrer sur un hôte spécifique", "start-on-host": "Démarrer sur un hôte spécifique",
"static-ip": "IP statique",
"stats": "Stats", "stats": "Stats",
"status": "Statut", "status": "Statut",
"storage": "Stockage", "storage": "Stockage",
@ -193,6 +229,15 @@
"vm-is-running": "La VM est en cours d'exécution", "vm-is-running": "La VM est en cours d'exécution",
"vms": "VMs", "vms": "VMs",
"xo-lite-under-construction": "XOLite est en construction", "xo-lite-under-construction": "XOLite est en construction",
"xoa-admin-account": "Compte administrateur de la XOA",
"xoa-deploy": "Déploiement de la XOA",
"xoa-deploy-failed": "Erreur lors du déploiement de la XOA !",
"xoa-deploy-retry": "Ré-essayer de déployer une XOA",
"xoa-deploy-successful": "XOA deployée avec succès !",
"xoa-ip": "XOA IP address",
"xoa-password-confirm-different": "La confirmation du mot de passe XOA est différente",
"xoa-ssh-account": "Compte SSH de la XOA",
"xoa-ssh-password-confirm-different": "La confirmation du mot de passe SSH est différente",
"you-are-currently-on": "Vous êtes actuellement sur : {0}", "you-are-currently-on": "Vous êtes actuellement sur : {0}",
"zstd": "zstd" "zstd": "zstd"
} }

View File

@ -12,6 +12,11 @@ const router = createRouter({
name: "home", name: "home",
component: HomeView, component: HomeView,
}, },
{
path: "/xoa-deploy",
name: "xoa.deploy",
component: () => import("@/views/xoa-deploy/XoaDeployView.vue"),
},
{ {
path: "/settings", path: "/settings",
name: "settings", name: "settings",

View File

@ -0,0 +1,9 @@
import { useXenApiStoreSubscribableContext } from "@/composables/xen-api-store-subscribable-context.composable";
import { createUseCollection } from "@/stores/xen-api/create-use-collection";
import { defineStore } from "pinia";
export const useNetworkStore = defineStore("xen-api-network", () => {
return useXenApiStoreSubscribableContext("network");
});
export const useNetworkCollection = createUseCollection(useNetworkStore);

View File

@ -0,0 +1,674 @@
<template>
<TitleBar :icon="faDownload">{{ $t("deploy-xoa") }}</TitleBar>
<div v-if="deploying" class="status">
<img src="@/assets/xo.svg" width="300" alt="Xen Orchestra" />
<!-- Error -->
<template v-if="error !== undefined">
<div>
<h2>{{ $t("xoa-deploy-failed") }}</h2>
<UiIcon :icon="faExclamationCircle" class="danger" />
</div>
<div class="error">
<strong>{{ $t("check-errors") }}</strong>
<UiRaw>{{ error }}</UiRaw>
</div>
<UiButton :icon="faDownload" @click="resetValues()">
{{ $t("xoa-deploy-retry") }}
</UiButton>
</template>
<!-- Success -->
<template v-else-if="url !== undefined">
<div>
<h2>{{ $t("xoa-deploy-successful") }}</h2>
<UiIcon :icon="faCircleCheck" class="success" />
</div>
<UiButton :icon="faArrowUpRightFromSquare" @click="openXoa">
{{ $t("access-xoa") }}
</UiButton>
</template>
<!-- Deploying -->
<template v-else>
<div>
<h2>{{ $t("xoa-deploy") }}</h2>
<!-- TODO: add progress bar -->
<p>{{ status }}</p>
</div>
<p class="warning">
<UiIcon :icon="faExclamationCircle" />
{{ $t("keep-page-open") }}
</p>
<UiButton
:disabled="vmRef === undefined"
color="error"
outlined
@click="cancel()"
>
{{ $t("cancel") }}
</UiButton>
</template>
</div>
<div v-else-if="isMobile" class="not-available">
<p>{{ $t("deploy-xoa-available-on-desktop") }}</p>
</div>
<div v-else class="card-view">
<UiCard>
<form @submit.prevent="deploy">
<FormSection :label="$t('configuration')">
<div class="row">
<FormInputWrapper
:label="$t('storage')"
:help="$t('n-gb-required', { n: REQUIRED_GB })"
>
<FormSelect v-model="selectedSr" required>
<option disabled :value="undefined">
{{ $t("select.storage") }}
</option>
<option
v-for="sr in filteredSrs"
:value="sr"
:key="sr.uuid"
:class="
sr.physical_size - sr.physical_utilisation <
REQUIRED_GB * 1024 ** 3
? 'warning'
: 'success'
"
>
{{ sr.name_label }} -
{{
$t("n-gb-left", {
n: Math.round(
(sr.physical_size - sr.physical_utilisation) / 1024 ** 3
),
})
}}
<span
v-if="
sr.physical_size - sr.physical_utilisation <
REQUIRED_GB * 1024 ** 3
"
></span
>
</option>
</FormSelect>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper :label="$t('network')" required>
<FormSelect v-model="selectedNetwork" required>
<option disabled :value="undefined">
{{ $t("select.network") }}
</option>
<option
v-for="network in filteredNetworks"
:value="network"
:key="network.uuid"
>
{{ network.name_label }}
</option>
</FormSelect>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper>
<div class="radio-group">
<label
><FormRadio value="static" v-model="ipStrategy" />{{
$t("static-ip")
}}</label
>
<label
><FormRadio value="dhcp" v-model="ipStrategy" />{{
$t("dhcp")
}}</label
>
</div>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper
:label="$t('xoa-ip')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
>
<FormInput
v-model="ip"
:disabled="!requireIpConf"
placeholder="xxx.xxx.xxx.xxx"
/>
</FormInputWrapper>
<FormInputWrapper
:label="$t('netmask')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
>
<FormInput
v-model="netmask"
:disabled="!requireIpConf"
placeholder="255.255.255.0"
/>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper
:label="$t('dns')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
>
<FormInput
v-model="dns"
:disabled="!requireIpConf"
placeholder="8.8.8.8"
/>
</FormInputWrapper>
<FormInputWrapper
:label="$t('gateway')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#network-configuration"
>
<FormInput
v-model="gateway"
:disabled="!requireIpConf"
placeholder="xxx.xxx.xxx.xxx"
/>
</FormInputWrapper>
</div>
</FormSection>
<FormSection :label="$t('xoa-admin-account')">
<div class="row">
<FormInputWrapper
:label="$t('admin-login')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
>
<FormInput
v-model="xoaUser"
required
placeholder="email@example.com"
/>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper
:label="$t('admin-password')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
>
<FormInput
type="password"
v-model="xoaPwd"
required
:placeholder="$t('password')"
/>
</FormInputWrapper>
<FormInputWrapper
:label="$t('admin-password-confirm')"
learnMoreUrl="https://xen-orchestra.com/docs/xoa.html#default-xo-account"
>
<FormInput
type="password"
v-model="xoaPwdConfirm"
required
:placeholder="$t('password')"
/>
</FormInputWrapper>
</div>
</FormSection>
<FormSection :label="$t('xoa-ssh-account')">
<div class="row">
<FormInputWrapper :label="$t('ssh-account')">
<label
><span>{{ $t("disabled") }}</span
><FormToggle v-model="enableSshAccount" /><span>{{
$t("enabled")
}}</span></label
>
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper :label="$t('ssh-login')">
<FormInput value="xoa" placeholder="xoa" disabled />
</FormInputWrapper>
</div>
<div class="row">
<FormInputWrapper :label="$t('ssh-password')">
<FormInput
type="password"
v-model="sshPwd"
:placeholder="$t('password')"
:disabled="!enableSshAccount"
:required="enableSshAccount"
/>
</FormInputWrapper>
<FormInputWrapper :label="$t('ssh-password-confirm')">
<FormInput
type="password"
v-model="sshPwdConfirm"
:placeholder="$t('password')"
:disabled="!enableSshAccount"
:required="enableSshAccount"
/>
</FormInputWrapper>
</div>
</FormSection>
<UiButtonGroup>
<UiButton outlined @click="router.back()">
{{ $t("cancel") }}
</UiButton>
<UiButton type="submit">
{{ $t("deploy") }}
</UiButton>
</UiButtonGroup>
</form>
</UiCard>
</div>
</template>
<script lang="ts" setup>
import { computed, ref } from "vue";
import {
faArrowUpRightFromSquare,
faCircleCheck,
faDownload,
faExclamationCircle,
} from "@fortawesome/free-solid-svg-icons";
import { storeToRefs } from "pinia";
import { useI18n } from "vue-i18n";
import { useModal } from "@/composables/modal.composable";
import { useNetworkCollection } from "@/stores/xen-api/network.store";
import { usePageTitleStore } from "@/stores/page-title.store";
import { useRouter } from "vue-router";
import { useSrCollection } from "@/stores/xen-api/sr.store";
import { useUiStore } from "@/stores/ui.store";
import { useXenApiStore } from "@/stores/xen-api.store";
import type { XenApiNetwork, XenApiSr } from "@/libs/xen-api/xen-api.types";
import FormInput from "@/components/form/FormInput.vue";
import FormInputWrapper from "@/components/form/FormInputWrapper.vue";
import FormRadio from "@/components/form/FormRadio.vue";
import FormSection from "@/components/form/FormSection.vue";
import FormSelect from "@/components/form/FormSelect.vue";
import FormToggle from "@/components/form/FormToggle.vue";
import TitleBar from "@/components/TitleBar.vue";
import UiButton from "@/components/ui/UiButton.vue";
import UiButtonGroup from "@/components/ui/UiButtonGroup.vue";
import UiCard from "@/components/ui/UiCard.vue";
import UiIcon from "@/components/ui/icon/UiIcon.vue";
import UiRaw from "@/components/ui/UiRaw.vue";
const REQUIRED_GB = 20;
const { t } = useI18n();
const router = useRouter();
usePageTitleStore().setTitle(() => t("deploy-xoa"));
const invalidField = (message: string) =>
useModal(() => import("@/components/modals/InvalidFieldModal.vue"), {
message,
});
const uiStore = useUiStore();
const { isMobile } = storeToRefs(uiStore);
const xapi = useXenApiStore().getXapi();
const { records: srs } = useSrCollection();
const filteredSrs = computed(() =>
srs.value
.filter((sr) => sr.content_type !== "iso" && sr.physical_size > 0)
// Sort: shared first then largest free space first
.sort((sr1, sr2) => {
if (sr1.shared === sr2.shared) {
return (
sr2.physical_size -
sr2.physical_utilisation -
(sr1.physical_size - sr1.physical_utilisation)
);
} else {
return sr1.shared ? -1 : 1;
}
})
);
const { records: networks } = useNetworkCollection();
const filteredNetworks = computed(() =>
[...networks.value].sort((network1, network2) =>
network1.name_label < network2.name_label ? -1 : 1
)
);
const deploying = ref(false);
const status = ref<string | undefined>();
const error = ref<string | undefined>();
const url = ref<string | undefined>();
const vmRef = ref<string | undefined>();
const resetValues = () => {
deploying.value = false;
status.value = undefined;
error.value = undefined;
url.value = undefined;
vmRef.value = undefined;
};
const openXoa = () => {
window.open(url.value, "_blank", "noopener");
};
const selectedSr = ref<XenApiSr>();
const selectedNetwork = ref<XenApiNetwork>();
const ipStrategy = ref<"static" | "dhcp">("dhcp");
const requireIpConf = computed(() => ipStrategy.value === "static");
const ip = ref("");
const netmask = ref("");
const dns = ref("");
const gateway = ref("");
const xoaUser = ref("");
const xoaPwd = ref("");
const xoaPwdConfirm = ref("");
const enableSshAccount = ref(true);
const sshPwd = ref("");
const sshPwdConfirm = ref("");
async function deploy() {
if (selectedSr.value === undefined || selectedNetwork.value === undefined) {
// Should not happen
console.error("SR or network is undefined");
return;
}
if (
ipStrategy.value === "static" &&
(ip.value === "" ||
netmask.value === "" ||
dns.value === "" ||
gateway.value === "")
) {
// Should not happen
console.error("Missing IP config");
return;
}
if (xoaUser.value === "" || xoaPwd.value === "") {
// Should not happen
console.error("Missing XOA credentials");
return;
}
if (xoaPwd.value !== xoaPwdConfirm.value) {
// TODO: use formal validation system
invalidField(t("xoa-password-confirm-different"));
return;
}
if (enableSshAccount.value && sshPwd.value === "") {
// Should not happen
console.error("Missing XOA credentials");
return;
}
if (enableSshAccount.value && sshPwd.value !== sshPwdConfirm.value) {
// TODO: use form validation system
invalidField(t("xoa-ssh-password-confirm-different"));
return;
}
deploying.value = true;
try {
status.value = t("deploy-xoa-status.importing");
vmRef.value = (
(await xapi.call("VM.import", [
"http://xoa.io:8888/",
selectedSr.value.$ref,
false, // full_restore
false, // force
])) as string[]
)[0];
status.value = t("deploy-xoa-status.configuring");
const [vifRef] = (await xapi.call("VM.get_VIFs", [
vmRef.value,
])) as string[];
await xapi.call("VIF.destroy", [vifRef]);
if (!deploying.value) {
return;
}
const [device] = (await xapi.call("VM.get_allowed_VIF_devices", [
vmRef.value,
])) as string[];
await xapi.call("VIF.create", [
{
device,
MAC: "",
MTU: selectedNetwork.value.MTU,
network: selectedNetwork.value.$ref,
other_config: {},
qos_algorithm_params: {},
qos_algorithm_type: "",
VM: vmRef.value,
},
]);
if (!deploying.value) {
return;
}
const promises = [
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/admin-account",
JSON.stringify({ email: xoaUser.value, password: xoaPwd.value }),
]),
];
// TODO: add host to servers with session token?
if (ipStrategy.value === "static") {
promises.push(
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/ip",
ip.value,
]),
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/netmask",
netmask.value,
]),
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/gateway",
gateway.value,
]),
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/dns",
dns.value,
])
);
}
if (enableSshAccount.value) {
promises.push(
xapi.call("VM.add_to_xenstore_data", [
vmRef.value,
"vm-data/system-account-xoa-password",
sshPwd.value,
])
);
}
await Promise.all(promises);
if (!deploying.value) {
return;
}
status.value = t("deploy-xoa-status.starting");
await xapi.call("VM.start", [
vmRef.value,
false, // start_paused
false, // force
]);
if (!deploying.value) {
return;
}
status.value = t("deploy-xoa-status.waiting");
const metricsRef = await xapi.call("VM.get_guest_metrics", [vmRef.value]);
let attempts = 120;
let networks: { "0/ip": string } | undefined;
await new Promise((resolve) => setTimeout(resolve, 10e3)); // Sleep 10s
do {
await new Promise((resolve) => setTimeout(resolve, 1e3)); // Sleep 1s
networks = await xapi.call("VM_guest_metrics.get_networks", [metricsRef]);
if (!deploying.value) {
return;
}
} while (--attempts > 0 && networks?.["0/ip"] === undefined);
if (attempts === 0 || networks === undefined) {
status.value = t("deploy-xoa-status.not-responding");
return;
}
await Promise.all(
[
"admin-account",
"dns",
"gateway",
"ip",
"netmask",
"xoa-updater-credentials",
].map((key) =>
xapi.call("VM.remove_from_xenstore_data", [
vmRef.value,
`vm-data/${key}`,
])
)
);
status.value = t("deploy-xoa-status.ready");
// TODO: handle IPv6
url.value = `https://${networks["0/ip"]}`;
} catch (err: any) {
console.error(err);
error.value = err?.message ?? err?.code ?? "Unknown error";
}
}
async function cancel() {
const _vmRef = vmRef.value;
console.log("_vmRef:", _vmRef);
resetValues();
if (_vmRef !== undefined) {
try {
await xapi.call("VM.destroy", [_vmRef]);
} catch (err) {
console.error(err);
}
}
}
</script>
<style lang="postcss" scoped>
.card-view {
flex-direction: column;
}
.row {
width: 100%;
display: flex;
flex-wrap: wrap;
column-gap: 10rem;
}
.form-toggle {
margin: 0 1.5rem;
}
.form-input-wrapper {
flex-grow: 1;
min-width: 60rem;
}
.input-container * {
vertical-align: middle;
}
.radio-group {
display: flex;
flex-direction: row;
margin: 1.67rem 0;
& > * {
min-width: 20rem;
}
}
.form-radio {
margin-right: 1rem;
}
.not-available,
.status {
display: flex;
flex-direction: column;
gap: 42px;
justify-content: center;
align-items: center;
min-height: 76.5vh;
color: var(--color-extra-blue-base);
text-align: center;
padding: 5rem;
margin: auto;
h2 {
margin-bottom: 1rem;
}
* {
max-width: 100%;
}
}
.not-available {
font-size: 2rem;
}
.status {
color: var(--color-blue-scale-100);
}
.success {
color: var(--color-green-infra-base);
}
.danger {
color: var(--color-red-vates-base);
}
.success,
.danger {
&.ui-icon {
font-size: 3rem;
}
}
.error {
display: flex;
flex-direction: column;
text-align: left;
gap: 0.5em;
}
.warning {
color: var(--color-orange-world-base);
}
</style>