diff --git a/.env b/.env index 2b07a77b..a9992747 100644 --- a/.env +++ b/.env @@ -9,3 +9,6 @@ DB_PORT=27017 #REDIS REDIS_HOST=redis REDIS_PORT=6379 +#SSM CONFIG +#SSM_INSTALL_PATH=/opt/squirrelserversmanager +#SSM_DATA_PATH=/data \ No newline at end of file diff --git a/.env.dev b/.env.dev index 2b07a77b..a9992747 100644 --- a/.env.dev +++ b/.env.dev @@ -9,3 +9,6 @@ DB_PORT=27017 #REDIS REDIS_HOST=redis REDIS_PORT=6379 +#SSM CONFIG +#SSM_INSTALL_PATH=/opt/squirrelserversmanager +#SSM_DATA_PATH=/data \ No newline at end of file diff --git a/client/src/components/Alert/AlertNotification.tsx b/client/src/components/Alert/AlertNotification.tsx index 25c930ce..7e68614e 100644 --- a/client/src/components/Alert/AlertNotification.tsx +++ b/client/src/components/Alert/AlertNotification.tsx @@ -1,12 +1,12 @@ import { socket } from '@/socket'; import { notification, Typography } from 'antd'; import React, { useEffect } from 'react'; -import { SsmEvents } from 'ssm-shared-lib'; +import { SsmEvents, SsmAlert } from 'ssm-shared-lib'; const AlertNotification: React.FC = () => { const [api, contextHolder] = notification.useNotification(); const openNotificationWithIcon = (payload: any) => { - if (payload?.severity === 'error') { + if (payload?.severity === SsmAlert.AlertType.ERROR) { api.error({ message: 'Alert', description: ( @@ -19,6 +19,19 @@ const AlertNotification: React.FC = () => { duration: 0, }); } + if (payload?.severity === SsmAlert.AlertType.SUCCESS) { + api.success({ + message: 'Success', + description: ( + + {payload?.message + ?.split('\n') + .map((line: string, index: number) =>

{line}

)} +
+ ), + duration: 0, + }); + } }; useEffect(() => { socket.connect(); diff --git a/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx index 1e6d2a84..b276611d 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/ContainerStacksGitRepositoryModal.tsx @@ -181,6 +181,10 @@ const ContainerStacksGitRepositoryModal: React.FC< await props.asyncFetch(); } else { await putContainerStacksGitRepository(values); + message.loading({ + content: 'Repository cloning & processing in process...', + duration: 6, + }); props.setModalOpened(false); await props.asyncFetch(); } diff --git a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx index 308b7222..651e7fd6 100644 --- a/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx +++ b/client/src/pages/Admin/Settings/components/subcomponents/PlaybooksGitRepositoryModal.tsx @@ -179,6 +179,10 @@ const PlaybooksGitRepositoryModal: React.FC< await props.asyncFetch(); } else { await putPlaybooksGitRepository(values); + message.loading({ + content: 'Repository cloning & processing in process...', + duration: 6, + }); props.setModalOpened(false); await props.asyncFetch(); } diff --git a/docker-compose.yml b/docker-compose.yml index 946e6675..b1532228 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -36,7 +36,8 @@ services: environment: NODE_ENV: production volumes: - - ./.data.prod:/data + - ./.data.prod/playbooks:/playbooks + - ./.data.prod/config:/ansible-config client: image: "ghcr.io/squirrelcorporation/squirrelserversmanager-client:latest" restart: unless-stopped diff --git a/server/Dockerfile b/server/Dockerfile index cee926da..7d05df64 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -3,6 +3,9 @@ LABEL org.opencontainers.image.source=https://github.com/SquirrelCorporation/Squ LABEL org.opencontainers.image.description="SSM Server" LABEL org.opencontainers.image.licenses="GNU AFFERO GENERAL PUBLIC LICENSE" +COPY entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + WORKDIR /opt/squirrelserversmanager/server RUN apk update && apk add ansible nmap sudo openssh sshpass py3-pip expect gcompat libcurl curl RUN apk add docker docker-cli-compose @@ -25,10 +28,12 @@ ENV NODE_ENV=production RUN npm ci --verbose --no-audit COPY . . RUN npm run build +ENTRYPOINT ["/entrypoint.sh"] CMD ["node", "./dist/src/index.js"] FROM base AS dev ENV NODE_ENV=development COPY ./nodemon.json . RUN npm install -g nodemon && npm ci --verbose --no-audit +ENTRYPOINT ["/entrypoint.sh"] CMD ["npm", "run", "dev"] diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100644 index 00000000..7d48555b --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Create symlinks +ln -sf /data/playbooks /playbooks +ln -sf /data /ansible-config + +# Execute the CMD +exec "$@" diff --git a/server/src/ansible/00000000-0000-0000-0000-000000000001/installers/install-docker.yml b/server/src/ansible/00000000-0000-0000-0000-000000000001/installers/install-docker.yml index 6fff8173..3f483eb1 100644 --- a/server/src/ansible/00000000-0000-0000-0000-000000000001/installers/install-docker.yml +++ b/server/src/ansible/00000000-0000-0000-0000-000000000001/installers/install-docker.yml @@ -1,121 +1,128 @@ -- name: Update apt package cache - ansible.builtin.apt: - update_cache: yes - when: ansible_os_family == "Debian" - -- name: Install required packages for Docker (Debian-based) - ansible.builtin.apt: - name: - - apt-transport-https - - ca-certificates - - curl - - gnupg-agent - - software-properties-common - state: present - when: ansible_os_family == "Debian" - -- name: Add Docker's official GPG key (Debian-based) - ansible.builtin.shell: | - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - when: ansible_os_family == "Debian" - -- name: Set up the Docker repository (Debian-based) - ansible.builtin.shell: | - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ - $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" |\ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - when: ansible_os_family == "Debian" - -- name: Update apt package cache after adding Docker repo - ansible.builtin.apt: - update_cache: yes - when: ansible_os_family == "Debian" - -- name: Install required packages for Docker (RHEL-based) - ansible.builtin.yum: - name: - - yum-utils - - device-mapper-persistent-data - - lvm2 - state: present - when: ansible_os_family == "RedHat" - -- name: Add Docker's official repository (RHEL-based) - ansible.builtin.command: | - yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo - when: ansible_os_family == "RedHat" - -- name: Ensure old versions of Docker are not installed (Debian-based) - ansible.builtin.apt: - name: "{{ item }}" - state: absent - loop: - - docker - - docker-engine - - docker.io - - containerd - - runc - when: ansible_os_family == "Debian" - -- name: Ensure old versions of Docker are not installed (RHEL-based) - ansible.builtin.yum: - name: "{{ item }}" - state: absent - loop: - - docker - - docker-common - - docker-selinux - - docker-engine - when: ansible_os_family == "RedHat" - -- name: Install Docker CE (Debian-based) - ansible.builtin.apt: - name: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-buildx-plugin - - docker-compose-plugin - state: present - update_cache: yes - when: ansible_os_family == "Debian" - -- name: Install Docker CE (RHEL-based) - ansible.builtin.yum: - name: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-buildx-plugin - - docker-compose-plugin - state: present - when: ansible_os_family == "RedHat" - -- name: Start and enable Docker service - ansible.builtin.service: - name: docker - state: started - enabled: yes - -- name: Add user to Docker group - ansible.builtin.user: - name: "{{ ansible_user_id }}" - groups: docker - append: yes - -- name: Check if user is part of docker group - command: groups {{ ansible_user_id }} - register: user_groups - changed_when: false - -- name: Display message if user is not yet in the Docker group - debug: - msg: "User '{{ ansible_user_id }}' needs to log out and log back in to apply the group change" - when: "'docker' not in user_groups.stdout" - -- name: Activate the changes to groups - command: newgrp docker - when: "'docker' not in user_groups.stdout" +--- +- name: Install Docker + hosts: all + become: true + gather_facts: false + + tasks: + - name: Update apt package cache + ansible.builtin.apt: + update_cache: yes + when: ansible_os_family == "Debian" + + - name: Install required packages for Docker (Debian-based) + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg-agent + - software-properties-common + state: present + when: ansible_os_family == "Debian" + + - name: Add Docker's official GPG key (Debian-based) + ansible.builtin.shell: | + sudo install -m 0755 -d /etc/apt/keyrings + sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + sudo chmod a+r /etc/apt/keyrings/docker.asc + when: ansible_os_family == "Debian" + + - name: Set up the Docker repository (Debian-based) + ansible.builtin.shell: | + echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ + $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" |\ + sudo tee /etc/apt/sources.list.d/docker.list > /dev/null + when: ansible_os_family == "Debian" + + - name: Update apt package cache after adding Docker repo + ansible.builtin.apt: + update_cache: yes + when: ansible_os_family == "Debian" + + - name: Install required packages for Docker (RHEL-based) + ansible.builtin.yum: + name: + - yum-utils + - device-mapper-persistent-data + - lvm2 + state: present + when: ansible_os_family == "RedHat" + + - name: Add Docker's official repository (RHEL-based) + ansible.builtin.command: | + yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo + when: ansible_os_family == "RedHat" + + - name: Ensure old versions of Docker are not installed (Debian-based) + ansible.builtin.apt: + name: "{{ item }}" + state: absent + loop: + - docker + - docker-engine + - docker.io + - containerd + - runc + when: ansible_os_family == "Debian" + + - name: Ensure old versions of Docker are not installed (RHEL-based) + ansible.builtin.yum: + name: "{{ item }}" + state: absent + loop: + - docker + - docker-common + - docker-selinux + - docker-engine + when: ansible_os_family == "RedHat" + + - name: Install Docker CE (Debian-based) + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: yes + when: ansible_os_family == "Debian" + + - name: Install Docker CE (RHEL-based) + ansible.builtin.yum: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + when: ansible_os_family == "RedHat" + + - name: Start and enable Docker service + ansible.builtin.service: + name: docker + state: started + enabled: yes + + - name: Add user to Docker group + ansible.builtin.user: + name: "{{ ansible_user_id }}" + groups: docker + append: yes + + - name: Check if user is part of docker group + command: groups {{ ansible_user_id }} + register: user_groups + changed_when: false + + - name: Display message if user is not yet in the Docker group + debug: + msg: "User '{{ ansible_user_id }}' needs to log out and log back in to apply the group change" + when: "'docker' not in user_groups.stdout" + + - name: Activate the changes to groups + command: newgrp docker + when: "'docker' not in user_groups.stdout" diff --git a/server/src/controllers/rest/playbooks/playbook.ts b/server/src/controllers/rest/playbooks/playbook.ts index 530cd664..cd0d785c 100644 --- a/server/src/controllers/rest/playbooks/playbook.ts +++ b/server/src/controllers/rest/playbooks/playbook.ts @@ -1,7 +1,9 @@ import PlaybookRepo from '../../../data/database/repository/PlaybookRepo'; +import logger from '../../../logger'; import { InternalError, NotFoundError } from '../../../middlewares/api/ApiError'; import { SuccessResponse } from '../../../middlewares/api/ApiResponse'; import Shell from '../../../modules/shell'; +import FileSystemManager from '../../../modules/shell/managers/FileSystemManager'; import PlaybooksRepositoryUseCases from '../../../services/PlaybooksRepositoryUseCases'; import PlaybookUseCases from '../../../services/PlaybookUseCases'; @@ -34,9 +36,14 @@ export const getPlaybook = async (req, res) => { if (!playbook) { throw new NotFoundError(`Playbook ${uuid} not found`); } + if (!Shell.PlaybookFileManager.testExistence(playbook.path)) { + throw new NotFoundError( + `Playbook ${playbook.name} not found on filesystem.\n This likely due to a de-synchronisation between the database and the filesystem.\n Please run "Sync To Database" to fix this issue. (In Settings/Playbooks)`, + ); + } try { const content = Shell.PlaybookFileManager.readPlaybook(playbook.path); - new SuccessResponse('Get Playbook successful', content).send(res); + new SuccessResponse('Got Playbook successfully', content).send(res); } catch (error: any) { throw new InternalError(error.message); } @@ -51,7 +58,7 @@ export const editPlaybook = async (req, res) => { } try { Shell.PlaybookFileManager.editPlaybook(req.body.content, playbook.path); - new SuccessResponse('Edit playbook successful').send(res); + new SuccessResponse('Playbook edited successfully').send(res); } catch (error: any) { throw new InternalError(error.message); } @@ -66,7 +73,7 @@ export const addExtraVarToPlaybook = async (req, res) => { } try { await PlaybookUseCases.addExtraVarToPlaybook(playbook, req.body.extraVar); - new SuccessResponse('Add extra var to playbook successful').send(res); + new SuccessResponse('Added an extra var to playbook successfully').send(res); } catch (error: any) { throw new InternalError(error.message); } @@ -81,7 +88,7 @@ export const deleteExtraVarFromPlaybook = async (req, res) => { } try { await PlaybookUseCases.deleteExtraVarFromPlaybook(playbook, varname); - new SuccessResponse('Delete extra var from playbook successful').send(res); + new SuccessResponse('Deleted extra var from playbook successfully').send(res); } catch (error: any) { throw new InternalError(error.message); } @@ -96,7 +103,7 @@ export const deletePlaybook = async (req, res) => { } try { await PlaybooksRepositoryUseCases.deletePlaybooksInRepository(playbook); - new SuccessResponse('Delete playbook successful').send(res); + new SuccessResponse('Playbook deleted successfully').send(res); } catch (error: any) { throw new InternalError(error.message); } diff --git a/server/src/core/events/EventManager.ts b/server/src/core/events/EventManager.ts index 93928a0d..912a95dc 100644 --- a/server/src/core/events/EventManager.ts +++ b/server/src/core/events/EventManager.ts @@ -6,7 +6,7 @@ type Listener = (...args: any[]) => void; export type Payload = { message: string; - severity: 'info' | 'warning' | 'error'; + severity: 'info' | 'warning' | 'error' | 'success'; module: string; moduleId?: string; }; diff --git a/server/src/modules/real-time/RealTime.ts b/server/src/modules/real-time/RealTime.ts index e8f85b66..9323608d 100644 --- a/server/src/modules/real-time/RealTime.ts +++ b/server/src/modules/real-time/RealTime.ts @@ -46,7 +46,7 @@ class RealTimeEngine extends EventManager { private createDebouncedEmitter(eventName: string, logMessage: string, debounceTime: number) { return debounce((payload: any) => { const io = App.getSocket().getIo(); - this.childLogger.info(`${logMessage}`); + this.childLogger.debug(`${logMessage}`); io.emit(eventName, payload); }, debounceTime); } diff --git a/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts index 801fe5e6..733ae8a2 100644 --- a/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts +++ b/server/src/modules/repository/ContainerCustomStacksRepositoryComponent.ts @@ -124,6 +124,11 @@ class ContainerCustomStacksRepositoryComponent extends EventManager { }), ); this.childLogger.info(`Updating Stacks Repository ${containerStackRepository.name}`); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully updated repository "${containerStackRepository.name}" with ${containerStackPathsToSync.length} files`, + module: 'ContainerCustomStackRepository', + }); } private async getContainerStackRepository() { @@ -231,6 +236,11 @@ class ContainerCustomStacksRepositoryComponent extends EventManager { }, }, }); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully commit and sync repository "${this.name}"`, + module: 'ContainerCustomStackRepository', + }); } catch (error: any) { this.childLogger.error(error); await GitCustomStacksRepositoryUseCases.putRepositoryOnError(this.uuid, error); @@ -260,6 +270,11 @@ class ContainerCustomStacksRepositoryComponent extends EventManager { }, }, }); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully forcepull repository ${this.name}`, + module: 'ContainerCustomStackRepository', + }); } catch (error: any) { this.childLogger.error(error); await GitCustomStacksRepositoryUseCases.putRepositoryOnError(this.uuid, error); diff --git a/server/src/modules/repository/PlaybooksRepositoryComponent.ts b/server/src/modules/repository/PlaybooksRepositoryComponent.ts index 7fe3fb53..2fdd67eb 100644 --- a/server/src/modules/repository/PlaybooksRepositoryComponent.ts +++ b/server/src/modules/repository/PlaybooksRepositoryComponent.ts @@ -2,6 +2,7 @@ import pino from 'pino'; import shell from 'shelljs'; import { SSM_DATA_PATH } from '../../config'; import EventManager from '../../core/events/EventManager'; +import Events from '../../core/events/events'; import { NotFoundError } from '../../middlewares/api/ApiError'; import Playbook from '../../data/database/model/Playbook'; import PlaybooksRepository from '../../data/database/model/PlaybooksRepository'; @@ -91,6 +92,7 @@ abstract class PlaybooksRepositoryComponent extends EventManager { this.childLogger.info( `Updating Playbooks Repository ${playbooksRepository.name} - ${playbooksRepository._id}`, ); + return playbooksToSync?.length; } private async getPlaybookRepository() { diff --git a/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts index 9a8e3970..3b50fb1f 100644 --- a/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts +++ b/server/src/modules/repository/git-playbooks-repository/GitPlaybooksRepositoryComponent.ts @@ -79,7 +79,12 @@ class GitPlaybooksRepositoryComponent }, }); if (syncAfter) { - await this.syncToDatabase(); + const nbSync = await this.syncToDatabase(); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully updated repository ${this.name} with ${nbSync} files`, + module: 'GitPlaybooksRepositoryComponent', + }); } } catch (error: any) { this.childLogger.error(error); @@ -109,6 +114,11 @@ class GitPlaybooksRepositoryComponent }, }, }); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully commit and sync repository ${this.name}`, + module: 'GitPlaybooksRepositoryComponent', + }); } catch (error: any) { this.childLogger.error(error); await GitPlaybooksRepositoryUseCases.putRepositoryOnError(this.uuid, error); @@ -137,6 +147,11 @@ class GitPlaybooksRepositoryComponent }, }, }); + this.emit(Events.ALERT, { + severity: SsmAlert.AlertType.SUCCESS, + message: `Successfully forcepull repository ${this.name}`, + module: 'GitPlaybooksRepositoryComponent', + }); } catch (error: any) { this.childLogger.error(error); await GitPlaybooksRepositoryUseCases.putRepositoryOnError(this.uuid, error); diff --git a/server/src/modules/shell/ShellWrapper.ts b/server/src/modules/shell/ShellWrapper.ts index d3a83596..82277eab 100644 --- a/server/src/modules/shell/ShellWrapper.ts +++ b/server/src/modules/shell/ShellWrapper.ts @@ -9,6 +9,7 @@ const ShellWrapper = { test: shell.test.bind(shell), chmod: shell.chmod.bind(shell), cp: shell.cp.bind(shell), + ln: shell.ln.bind(shell), to: (content: string, path: string) => shell.ShellString(content).to(path), }; diff --git a/shared-lib/src/enums/alert.ts b/shared-lib/src/enums/alert.ts index 66160b7a..7aebc6dd 100644 --- a/shared-lib/src/enums/alert.ts +++ b/shared-lib/src/enums/alert.ts @@ -1,3 +1,6 @@ export enum AlertType { - ERROR = 'error' + ERROR = 'error', + SUCCESS = 'success', + WARNING = 'warning', + INFO = 'info', } diff --git a/site/docs/install/dockerless.md b/site/docs/install/dockerless.md index 5d51afec..169da2d6 100644 --- a/site/docs/install/dockerless.md +++ b/site/docs/install/dockerless.md @@ -30,6 +30,8 @@ apk add libcurl apk add gcompat apk add curl apk add newt +$STD apk add docker +$STD apk add docker-cli-compose ``` Verify the installations: @@ -121,6 +123,12 @@ Clone the repository: git clone https://github.com/SquirrelCorporation/SquirrelServersManager.git /opt/squirrelserversmanager ``` +Install deps: +```sh +rm -f /usr/lib/python3.12/EXTERNALLY-MANAGED +pip install ansible-runner ansible-runner-http +``` + Generate secrets and create the environment file: ```sh @@ -139,6 +147,9 @@ DB_PORT=27017 # REDIS REDIS_HOST=127.0.0.1 REDIS_PORT=6379 +# SSM CONFIG +SSM_INSTALL_PATH=/opt/squirrelserversmanager +SSM_DATA_PATH=/opt/squirrelserversmanager/data EOF ```