feat(lite): XOA quick deploy (#7245)
This commit is contained in:
parent
cc080ec681
commit
b0e000328d
@ -7,6 +7,7 @@
|
||||
- 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 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))
|
||||
|
||||
## **0.1.6** (2023-11-30)
|
||||
|
@ -21,7 +21,8 @@ a {
|
||||
}
|
||||
|
||||
code,
|
||||
code * {
|
||||
code *,
|
||||
pre {
|
||||
font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
||||
"Courier New", monospace;
|
||||
}
|
||||
|
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
1
@xen-orchestra/lite/src/assets/xo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 43 KiB |
@ -13,6 +13,9 @@
|
||||
<slot />
|
||||
<div class="right">
|
||||
<PoolOverrideWarning as-tooltip />
|
||||
<UiButton v-if="isDesktop" :icon="faDownload" @click="openXoaDeploy">
|
||||
{{ $t("deploy-xoa") }}
|
||||
</UiButton>
|
||||
<AccountButton />
|
||||
</div>
|
||||
</header>
|
||||
@ -22,14 +25,20 @@
|
||||
import AccountButton from "@/components/AccountButton.vue";
|
||||
import PoolOverrideWarning from "@/components/PoolOverrideWarning.vue";
|
||||
import TextLogo from "@/components/TextLogo.vue";
|
||||
import UiButton from "@/components/ui/UiButton.vue";
|
||||
import UiIcon from "@/components/ui/icon/UiIcon.vue";
|
||||
import { useNavigationStore } from "@/stores/navigation.store";
|
||||
import { useRouter } from "vue-router";
|
||||
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";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const openXoaDeploy = () => router.push({ name: "xoa.deploy" });
|
||||
|
||||
const uiStore = useUiStore();
|
||||
const { isMobile } = storeToRefs(uiStore);
|
||||
const { isMobile, isDesktop } = storeToRefs(uiStore);
|
||||
|
||||
const navigationStore = useNavigationStore();
|
||||
const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
@ -62,5 +71,6 @@ const { trigger: navigationTrigger } = storeToRefs(navigationStore);
|
||||
.right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
@ -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>
|
18
@xen-orchestra/lite/src/components/ui/UiRaw.vue
Normal file
18
@xen-orchestra/lite/src/components/ui/UiRaw.vue
Normal 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>
|
@ -54,6 +54,7 @@ type ObjectTypeToRecordMapping = {
|
||||
host: XenApiHost;
|
||||
host_metrics: XenApiHostMetrics;
|
||||
message: XenApiMessage<any>;
|
||||
network: XenApiNetwork;
|
||||
pool: XenApiPool;
|
||||
sr: XenApiSr;
|
||||
vm: XenApiVm;
|
||||
@ -113,9 +114,11 @@ export interface XenApiHost extends XenApiRecord<"host"> {
|
||||
}
|
||||
|
||||
export interface XenApiSr extends XenApiRecord<"sr"> {
|
||||
content_type: string;
|
||||
name_label: string;
|
||||
physical_size: number;
|
||||
physical_utilisation: number;
|
||||
shared: boolean;
|
||||
}
|
||||
|
||||
export interface XenApiVm extends XenApiRecord<"vm"> {
|
||||
|
@ -1,9 +1,13 @@
|
||||
{
|
||||
"about": "About",
|
||||
"access-xoa": "Access XOA",
|
||||
"add": "Add",
|
||||
"add-filter": "Add filter",
|
||||
"add-or": "+OR",
|
||||
"add-sort": "Add sort",
|
||||
"admin-login": "Admin login",
|
||||
"admin-password": "Admin password",
|
||||
"admin-password-confirm": "Confirm admin password",
|
||||
"alarm-type": {
|
||||
"cpu_usage": "CPU usage exceeds {n}%",
|
||||
"disk_usage": "Disk usage exceeds {n}%",
|
||||
@ -26,12 +30,14 @@
|
||||
"backup": "Backup",
|
||||
"cancel": "Cancel",
|
||||
"change-state": "Change state",
|
||||
"check-errors": "Check out the errors:",
|
||||
"click-to-display-alarms": "Click to display alarms:",
|
||||
"click-to-return-default-pool": "Click here to return to the default pool",
|
||||
"close": "Close",
|
||||
"coming-soon": "Coming soon!",
|
||||
"community": "Community",
|
||||
"community-name": "{name} community",
|
||||
"configuration": "Configuration",
|
||||
"confirm-cancel": "Are you sure you want to cancel?",
|
||||
"confirm-delete": "You're about to delete {0}",
|
||||
"console": "Console",
|
||||
@ -43,14 +49,28 @@
|
||||
"dashboard": "Dashboard",
|
||||
"delete": "Delete",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"dhcp": "DHCP",
|
||||
"disabled": "Disabled",
|
||||
"display": "Display",
|
||||
"dns": "DNS",
|
||||
"do-you-have-needs": "You have needs and/or expectations? Let us know",
|
||||
"documentation": "Documentation",
|
||||
"documentation-name": "{name} documentation",
|
||||
"edit-config": "Edit config",
|
||||
"enabled": "Enabled",
|
||||
"error-no-data": "Error, can't collect data.",
|
||||
"error-occurred": "An error has occurred",
|
||||
"export": "Export",
|
||||
@ -84,11 +104,16 @@
|
||||
"force-shutdown": "Force shutdown",
|
||||
"fullscreen": "Fullscreen",
|
||||
"fullscreen-leave": "Leave fullscreen",
|
||||
"gateway": "Gateway",
|
||||
"n-gb-left": "{n} GB left",
|
||||
"n-gb-required": "{n} GB required",
|
||||
"go-back": "Go back",
|
||||
"gzip": "gzip",
|
||||
"here": "Here",
|
||||
"hosts": "Hosts",
|
||||
"invalid-field": "Invalid field",
|
||||
"keep-me-logged": "Keep me logged in",
|
||||
"keep-page-open": "Do not refresh or quit tab before end of deployment.",
|
||||
"language": "Language",
|
||||
"last-week": "Last week",
|
||||
"learn-more": "Learn more",
|
||||
@ -104,6 +129,7 @@
|
||||
"n-missing": "{n} missing",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Name",
|
||||
"netmask": "Netmask",
|
||||
"network": "Network",
|
||||
"network-download": "Download",
|
||||
"network-throughput": "Network throughput",
|
||||
@ -119,6 +145,7 @@
|
||||
"not-found": "Not found",
|
||||
"object": "Object",
|
||||
"object-not-found": "Object {id} can't be found…",
|
||||
"ok": "OK",
|
||||
"on-object": "on {object}",
|
||||
"open-console-in-new-tab": "Open console in new tab",
|
||||
"or": "Or",
|
||||
@ -154,14 +181,23 @@
|
||||
"selected-vms-in-execution": "Some selected VMs are running",
|
||||
"send-ctrl-alt-del": "Send Ctrl+Alt+Del",
|
||||
"send-us-feedback": "Send us feedback",
|
||||
"select": {
|
||||
"network": "Select a network",
|
||||
"storage": "Select a storage"
|
||||
},
|
||||
"settings": "Settings",
|
||||
"shutdown": "Shutdown",
|
||||
"snapshot": "Snapshot",
|
||||
"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-ram-usage": "Stacked RAM usage",
|
||||
"start": "Start",
|
||||
"start-on-host": "Start on specific host",
|
||||
"static-ip": "Static IP",
|
||||
"stats": "Stats",
|
||||
"status": "Status",
|
||||
"storage": "Storage",
|
||||
@ -193,6 +229,15 @@
|
||||
"vm-is-running": "The VM is running",
|
||||
"vms": "VMs",
|
||||
"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}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
{
|
||||
"about": "À propos",
|
||||
"access-xoa": "Accéder à la XOA",
|
||||
"add": "Ajouter",
|
||||
"add-filter": "Ajouter un filtre",
|
||||
"add-or": "+OU",
|
||||
"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": {
|
||||
"cpu_usage": "L'utilisation du CPU dépasse {n}%",
|
||||
"disk_usage": "L'utilisation du disque dépasse {n}%",
|
||||
@ -26,12 +30,14 @@
|
||||
"backup": "Sauvegarde",
|
||||
"cancel": "Annuler",
|
||||
"change-state": "Changer l'état",
|
||||
"check-errors": "Consultez les erreurs :",
|
||||
"click-to-display-alarms": "Cliquer pour afficher les alarmes :",
|
||||
"click-to-return-default-pool": "Cliquer ici pour revenir au pool par défaut",
|
||||
"close": "Fermer",
|
||||
"coming-soon": "Bientôt disponible !",
|
||||
"community": "Communauté",
|
||||
"community-name": "Communauté {name}",
|
||||
"configuration": "Configuration",
|
||||
"confirm-cancel": "Êtes-vous sûr de vouloir annuler ?",
|
||||
"confirm-delete": "Vous êtes sur le point de supprimer {0}",
|
||||
"console": "Console",
|
||||
@ -43,14 +49,28 @@
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete": "Supprimer",
|
||||
"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",
|
||||
"description": "Description",
|
||||
"dhcp": "DHCP",
|
||||
"dns": "DNS",
|
||||
"disabled": "Désactivé",
|
||||
"display": "Affichage",
|
||||
"do-you-have-needs": "Vous avez des besoins et/ou des attentes ? Faites le nous savoir",
|
||||
"documentation": "Documentation",
|
||||
"documentation-name": "Documentation {name}",
|
||||
"edit-config": "Modifier config",
|
||||
"enabled": "Activé",
|
||||
"error-no-data": "Erreur, impossible de collecter les données.",
|
||||
"error-occurred": "Une erreur est survenue",
|
||||
"export": "Exporter",
|
||||
@ -84,11 +104,16 @@
|
||||
"force-shutdown": "Forcer l'arrêt",
|
||||
"fullscreen": "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",
|
||||
"gzip": "gzip",
|
||||
"here": "Ici",
|
||||
"hosts": "Hôtes",
|
||||
"invalid-field": "Champ invalide",
|
||||
"keep-me-logged": "Rester connecté",
|
||||
"keep-page-open": "Ne pas rafraichir ou quitter cette page avant la fin du déploiement.",
|
||||
"language": "Langue",
|
||||
"last-week": "Semaine dernière",
|
||||
"learn-more": "En savoir plus",
|
||||
@ -104,6 +129,7 @@
|
||||
"n-missing": "{n} manquant | {n} manquants",
|
||||
"n-vms": "1 VM | {n} VMs",
|
||||
"name": "Nom",
|
||||
"netmask": "Masque réseau",
|
||||
"network": "Réseau",
|
||||
"network-download": "Descendant",
|
||||
"network-throughput": "Débit du réseau",
|
||||
@ -119,6 +145,7 @@
|
||||
"not-found": "Non trouvé",
|
||||
"object": "Objet",
|
||||
"object-not-found": "L'objet {id} est introuvable…",
|
||||
"ok": "OK",
|
||||
"on-object": "sur {object}",
|
||||
"open-console-in-new-tab": "Ouvrir la console dans un nouvel onglet",
|
||||
"or": "Ou",
|
||||
@ -154,14 +181,23 @@
|
||||
"selected-vms-in-execution": "Certaines VMs sélectionnées sont en cours d'exécution",
|
||||
"send-ctrl-alt-del": "Envoyer Ctrl+Alt+Suppr",
|
||||
"send-us-feedback": "Envoyez-nous vos commentaires",
|
||||
"select": {
|
||||
"network": "Sélectionner un réseau",
|
||||
"storage": "Sélectionner un SR"
|
||||
},
|
||||
"settings": "Paramètres",
|
||||
"shutdown": "Arrêter",
|
||||
"snapshot": "Instantané",
|
||||
"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-ram-usage": "Utilisation RAM empilée",
|
||||
"start": "Démarrer",
|
||||
"start-on-host": "Démarrer sur un hôte spécifique",
|
||||
"static-ip": "IP statique",
|
||||
"stats": "Stats",
|
||||
"status": "Statut",
|
||||
"storage": "Stockage",
|
||||
@ -193,6 +229,15 @@
|
||||
"vm-is-running": "La VM est en cours d'exécution",
|
||||
"vms": "VMs",
|
||||
"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}",
|
||||
"zstd": "zstd"
|
||||
}
|
||||
|
@ -12,6 +12,11 @@ const router = createRouter({
|
||||
name: "home",
|
||||
component: HomeView,
|
||||
},
|
||||
{
|
||||
path: "/xoa-deploy",
|
||||
name: "xoa.deploy",
|
||||
component: () => import("@/views/xoa-deploy/XoaDeployView.vue"),
|
||||
},
|
||||
{
|
||||
path: "/settings",
|
||||
name: "settings",
|
||||
|
9
@xen-orchestra/lite/src/stores/xen-api/network.store.ts
Normal file
9
@xen-orchestra/lite/src/stores/xen-api/network.store.ts
Normal 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);
|
674
@xen-orchestra/lite/src/views/xoa-deploy/XoaDeployView.vue
Normal file
674
@xen-orchestra/lite/src/views/xoa-deploy/XoaDeployView.vue
Normal 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>
|
Loading…
Reference in New Issue
Block a user