diff --git a/src/core/app/database.py b/src/core/app/database.py index 7a5366e6..bb50cafb 100644 --- a/src/core/app/database.py +++ b/src/core/app/database.py @@ -66,7 +66,7 @@ class Hosts(SQLModel, table=True): hostname: str ipaddress: str username: Optional[str] = None - ssh: Optional[bool] = 0 + ssh: Optional[int] = 0 pool_id: Optional[UUID] = Field(default=None, foreign_key="pools.id") tags: Optional[str] = None state: Optional[bool] = 0 @@ -77,7 +77,7 @@ def to_json(self): "hostname": self.hostname, "ipaddress": self.ipaddress, "username": self.username, - "ssh": bool(self.ssh), + "ssh": int(self.ssh), "pool_id": str(self.pool_id), "tags": self.tags, "state": bool(self.state) diff --git a/src/core/app/kvm/kvm_check.py b/src/core/app/kvm/kvm_check.py index ef193a61..0810108e 100644 --- a/src/core/app/kvm/kvm_check.py +++ b/src/core/app/kvm/kvm_check.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +# THIS FILE DOESN'T SEEM TO BE USED ANYWHERE IN THE PROJECT + from sqlmodel import Session, select from fastapi.encoders import jsonable_encoder from app.patch import ensure_uuid diff --git a/src/core/app/routes/host.py b/src/core/app/routes/host.py index 3989e087..0f9ac8d1 100644 --- a/src/core/app/routes/host.py +++ b/src/core/app/routes/host.py @@ -171,20 +171,19 @@ def retrieve_host(): engine = database.init_db_connection() try: - records = [] with Session(engine) as session: statement = select(Hosts) results = session.exec(statement) - for host in results: - records.append(host) - for host in records: + hosts = session.exec(statement).all() + + for host in hosts: try: shell.os_system(f"nc -z -w 1 {host.ipaddress} 22 > /dev/null") host.state = 'Reachable' except shell.ShellException: # TODO Be more precise than before and check the exit code ? host.state = 'Unreachable' - return jsonable_encoder(records) + return jsonable_encoder(hosts) except Exception as e: raise ValueError(e) diff --git a/src/core/app/ssh.py b/src/core/app/ssh.py index b5ae73ec..875f855e 100644 --- a/src/core/app/ssh.py +++ b/src/core/app/ssh.py @@ -114,31 +114,39 @@ def __init__(self, message): def init_ssh_connection(host_id, ip_address, username): shell.subprocess_run(f"ls -al {__get_local_ssh_directory().as_posix()}") - client = paramiko.SSHClient() - client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - - try: - client.connect( - hostname=ip_address, - username=username, - ) - client.close() - except OSError: - raise ConnectionException( - "The hypervisor is unreachable.") - except paramiko.ssh_exception.AuthenticationException: - raise ConnectionException( - "Authentication to the hypervisor has failed.") - - host.filter_host_by_id(host_id) engine = database.init_db_connection() - with Session(engine) as session: statement = select(Hosts).where(Hosts.id == ensure_uuid(host_id)) results = session.exec(statement) data_host = results.one() - data_host.ssh = 1 data_host.username = username + + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + client.connect( + hostname=ip_address, + username=username, + ) + client.close() + data_host.ssh = 1 + except OSError: + data_host.ssh = 2 + session.add(data_host) + session.commit() + session.refresh(data_host) + raise ConnectionException( + "The hypervisor is unreachable.") + except paramiko.ssh_exception.AuthenticationException: + data_host.ssh = 2 + session.add(data_host) + session.commit() + session.refresh(data_host) + raise ConnectionException( + "Authentication to the hypervisor has failed.") + + host.filter_host_by_id(host_id) + session.add(data_host) session.commit() session.refresh(data_host) diff --git a/src/ui/src/pages/admin/resources/hypervisors/HypervisorForm.vue b/src/ui/src/pages/admin/resources/hypervisors/HypervisorForm.vue index 987b71e7..735ac916 100644 --- a/src/ui/src/pages/admin/resources/hypervisors/HypervisorForm.vue +++ b/src/ui/src/pages/admin/resources/hypervisors/HypervisorForm.vue @@ -2,26 +2,48 @@ + :title=" + hypersivorId + ? `Updating hypervisor ${stateHypervisor?.hostname ?? ''}` + : 'Adding hypervisor' + " + /> - +
- +
- +

- + {{ hypervisorId ? "Update" : "Add" }}
@@ -40,7 +62,7 @@ import FormHeader from "@/components/forms/FormHeader.vue"; export default { components: { ...spinners, - FormHeader + FormHeader, }, data() { return { @@ -129,8 +151,8 @@ export default { color: "success", }); }) - .catch(error => { - console.error(error) + .catch((error) => { + console.error(error); this.$vaToast.init({ title: "Unable to add hypervisor", message: error?.response?.data?.detail ?? error, diff --git a/src/ui/src/pages/admin/resources/hypervisors/HypervisorList.vue b/src/ui/src/pages/admin/resources/hypervisors/HypervisorList.vue index 5c707ae6..68993334 100644 --- a/src/ui/src/pages/admin/resources/hypervisors/HypervisorList.vue +++ b/src/ui/src/pages/admin/resources/hypervisors/HypervisorList.vue @@ -2,16 +2,29 @@
- + - + @@ -20,43 +33,87 @@ {{ value }} - +
- +
@@ -69,9 +126,14 @@
- +
- - + + " + />
@@ -109,8 +182,20 @@
You are about to remove hypervisor - {{ JSON.parse(JSON.stringify(this.selectedHost)).hostname }}.
Please confirm action. + {{ JSON.parse(JSON.stringify(this.selectedHost)).hostname }}.
Please confirm action.
+ +
@@ -134,8 +219,9 @@ export default defineComponent({ { key: "hostname" }, { key: "pool_id", label: "Pool", sortable: true }, { key: "ipaddress" }, - { key: "ssh", label: "SSH Connection" }, { key: "tags", sortable: true }, + { key: "ssh", label: "SSH Connection" }, + { key: "state", sortable: true }, { key: "actions" }, ], @@ -147,8 +233,18 @@ export default defineComponent({ showConnectModal: false, showDeleteModal: false, selectedHost: null, + forceDelete: false, + refreshInterval: null, }; }, + mounted() { + this.requestKeys(); + this.$store.dispatch("requestHost"); + this.startRefreshing(); + }, + beforeUnmount() { + this.stopRefreshing(); + }, computed: { areDependenciesResolved() { // Prevent showing irrelevant alert by checking if the table is ready. @@ -169,6 +265,7 @@ export default defineComponent({ showConnectModal(newValue, oldValue) { if (newValue && !oldValue) { this.isKeyCopied = false; + this.user = this.selectedHost?.username || null; } }, currentTabKey() { @@ -210,36 +307,68 @@ export default defineComponent({ }, connectHost() { if (this.validation) { - axios - .post( - `${this.$store.state.endpoint.api}/api/v1/connect/${this.selectedHost.id}`, - { ip_address: this.selectedHost.ipaddress, username: this.user }, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.$store.state.token}`, - }, - } - ) - .then((response) => { - this.$store.dispatch("requestHost"); + this.refreshConnectHost(); + } else { + this.$vaToast.init({ + title: "Validation Error", + message: "Please fill in the required fields.", + color: "warning", + }); + } + }, + + refreshConnectHost( + host = this.selectedHost, + user = this.user, + silent = false + ) { + // const username = this.user || this.selectedHost?.username; + const username = this.user; + return axios + .post( + `${this.$store.state.endpoint.api}/api/v1/connect/${host.id}`, + { ip_address: host.ipaddress, username: user }, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.$store.state.token}`, + }, + } + ) + .then((response) => { + this.$store.dispatch("requestHost", { + token: this.$store.state.token, + }); + if (!silent) { this.$vaToast.init({ title: response.data.state, - message: `Successfully connected to ${this.selectedHost.hostname}`, + message: `Successfully connected to ${host.hostname} with ${user} user.`, color: "success", }); - this.showConnectModal = false; - }) - .catch((error) => { - console.error(error); + } + this.showConnectModal = false; + }) + .catch((error) => { + console.error(error); + if (!silent) { + let message; + if (host.ssh === 0) { + message = `SSH connection is not configured for ${host.hostname}. Please link the SSH connection.`; + } else if (host.ssh === 2) { + message = `Unable to connect to ${host.hostname} with ${user} user.`; + } else { + message = `Connection error for ${host.hostname}.`; + } + this.$vaToast.init({ title: "Error", - message: error?.response?.data?.detail ?? error, + message, color: "danger", }); - }); - } + } + }); }, + requestKeys() { axios .get(`${this.$store.state.endpoint.api}/api/v1/publickeys`, { @@ -264,16 +393,23 @@ export default defineComponent({ }); }, deleteHost() { - axios - .delete( - `${this.$store.state.endpoint.api}/api/v1/hosts/${this.selectedHost.id}`, - { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.$store.state.token}`, - }, - } - ) + this.refreshConnectHost( + this.selectedHost, + this.selectedHost.username, + true + ) + .catch(() => {}) + .then(() => { + return axios.delete( + `${this.$store.state.endpoint.api}/api/v1/hosts/${this.selectedHost.id}`, + { + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${this.$store.state.token}`, + }, + } + ); + }) .then((response) => { this.$store.dispatch("requestHost"); this.$vaToast.init({ @@ -281,6 +417,15 @@ export default defineComponent({ message: "Hypervisor has been successfully deleted", color: "success", }); + + if (this.selectedHost.ssh === 2) { + this.$vaToast.init({ + title: "Warning", + message: + "SSH keys may not have been removed from the hypervisor (SSH connection was in error state).", + color: "warning", + }); + } }) .catch((error) => { console.error(error); @@ -291,10 +436,26 @@ export default defineComponent({ }); }); }, - }, - mounted() { - this.requestKeys(); - this.$store.dispatch("requestHost"); + startRefreshing() { + this.refreshInterval = setInterval(() => { + this.refreshAllHosts(); + }, 5000); + }, + stopRefreshing() { + if (this.refreshInterval) { + clearInterval(this.refreshInterval); + this.refreshInterval = null; + } + }, + refreshAllHosts() { + const hosts = this.$store.state.resources.hostList; + if (!hosts || hosts.length === 0) return; + + hosts.forEach((host) => { + const username = host.username; + this.refreshConnectHost(host, username, true); + }); + }, }, }); diff --git a/src/ui/src/store/index.ts b/src/ui/src/store/index.ts index 9fe8a233..71153de6 100644 --- a/src/ui/src/store/index.ts +++ b/src/ui/src/store/index.ts @@ -2,6 +2,10 @@ import axios from "axios"; import { createStore } from "vuex"; import router from "../router"; +interface Host { + id: string | number; +} + export default createStore({ strict: true, // process.env.NODE_ENV !== 'production', state: { @@ -27,7 +31,7 @@ export default createStore({ resources: { policyList: [], poolList: [], - hostList: [], + hostList: [] as Host[], vmList: [], externalHookList: [], connectorList: [], @@ -640,6 +644,12 @@ export default createStore({ loadingHost(state, loadingState) { state.isHostTableReady = loadingState; }, + hostLocalDeletion(state, hostId) { + const index = state.resources.hostList.findIndex((h) => h.id === hostId); + if (index !== -1) { + state.resources.hostList.splice(index, 1); + } + }, vmList(state, vmList) { state.resources.vmList = vmList; },