diff --git a/client/package.json b/client/package.json index dab237c6..f6623915 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "ssm-client", - "version": "0.1.20", + "version": "0.1.21", "private": true, "description": "SSM Client - A simple way to manage all your servers", "author": "Squirrel Team", diff --git a/client/src/app.tsx b/client/src/app.tsx index 6988f4fc..37679928 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -8,6 +8,7 @@ import DocumentationWidget from '@/components/HeaderComponents/DocumentationWidg import { HealthWidget } from '@/components/HeaderComponents/HealthWidget'; import NotificationsWidget from '@/components/HeaderComponents/NotificationsWidget'; import UpdateAvailableWidget from '@/components/HeaderComponents/UpdateAvailableWidget'; +import NoDeviceModal from '@/components/NoDevice/NoDeviceModal'; import { currentUser as queryCurrentUser, hasUser } from '@/services/rest/user'; import type { Settings as LayoutSettings } from '@ant-design/pro-components'; // @ts-ignore @@ -123,8 +124,13 @@ export const layout: RunTimeLayoutConfig = ({ description={`The server version (${initialState?.currentUser?.settings?.server.version}) does not match the client version (${version}). You may need to retry a docker compose pull to update SSM.`} type="warning" showIcon + banner /> )} + {initialState?.currentUser?.devices?.overview && + initialState?.currentUser?.devices?.overview?.length === 0 && ( + + )} {children} ); diff --git a/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx b/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx index f9f6d234..83237ddc 100644 --- a/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx +++ b/client/src/components/DeviceConfiguration/CheckDeviceConnection.tsx @@ -1,11 +1,18 @@ -import TerminalHandler from '@/components/PlaybookExecutionModal/PlaybookExecutionHandler'; +import SwitchConnexionMethod from '@/components/NewDeviceModal/SwitchConnexionMethod'; +import TerminalHandler, { + TaskStatusTimelineType, +} from '@/components/PlaybookExecutionModal/PlaybookExecutionHandler'; +import { getAnsibleSmartFailure } from '@/services/rest/ansible'; import { CheckCircleOutlined, + ClockCircleOutlined, CloseOutlined, InfoCircleFilled, LoadingOutlined, + SwitcherOutlined, } from '@ant-design/icons'; -import { Popover, Steps, Typography } from 'antd'; +import { Alert, Button, Popover, Steps, Typography } from 'antd'; +import { motion } from 'framer-motion'; import React, { useEffect, useRef, useState } from 'react'; import { API } from 'ssm-shared-lib'; @@ -15,6 +22,18 @@ export type CheckDeviceConnectionProps = { dockerConnErrorMessage?: string; }; +const taskInit: TaskStatusTimelineType = { + _status: 'created', + status: 'finish', + icon: , + title: 'created', +}; + +const animationVariants = { + hidden: { opacity: 0, y: -20 }, + visible: { opacity: 1, y: 0 }, +}; + const CheckDeviceConnection: React.FC = (props) => { const { execId, dockerConnRes, dockerConnErrorMessage } = props; const timerIdRef = useRef(); @@ -22,9 +41,25 @@ const CheckDeviceConnection: React.FC = (props) => { const [playbookStatus, setPlaybookStatus] = useState('running...'); const [dockerStatus, setDockerStatus] = useState('running...'); const [execRes, setExecRes] = useState(<>); + const [smartFailure, setSmartFailure] = useState< + API.SmartFailure | undefined + >(); + const statusesType: TaskStatusTimelineType[] = [taskInit]; + const [savedStatuses, setSavedStatuses] = useState(statusesType); const statusCallback = (status: string) => { setPlaybookStatus(status); }; + const [count, setCount] = useState(0); + + const isFinalStatusFailed = async () => { + if (savedStatuses?.find((status) => status._status === 'failed')) { + const res = await getAnsibleSmartFailure({ execId: execId }); + if (res.data) { + setSmartFailure(res.data); + } + return true; + } + }; const execLogsCallBack = (execLog: API.ExecLog) => { setExecRes((previous) => ( <> @@ -43,8 +78,13 @@ const CheckDeviceConnection: React.FC = (props) => { undefined, execLogsCallBack, statusCallback, + setSavedStatuses, ); + useEffect(() => { + void isFinalStatusFailed(); + }, [savedStatuses]); + useEffect(() => { if (execId) { terminalHandler.resetTerminal(); @@ -66,13 +106,17 @@ const CheckDeviceConnection: React.FC = (props) => { }, [dockerConnRes]); useEffect(() => { - const pollingCallback = () => terminalHandler.pollingCallback(execId || ''); + const pollingCallback = () => { + terminalHandler.pollingCallback(execId || ''); + setCount((prevCount) => prevCount + 1); + }; const startPolling = () => { - // pollingCallback(); // To immediately start fetching data + setCount(0); // Polling every 3 seconds // @ts-ignore timerIdRef.current = setInterval(pollingCallback, 3000); + setSmartFailure(undefined); }; const stopPolling = () => { @@ -90,72 +134,107 @@ const CheckDeviceConnection: React.FC = (props) => { }; }, [isPollingEnabled]); return ( - - {playbookStatus}{' '} - {playbookStatus === 'failed' && ( - - {execRes} - - } - overlayStyle={{ - width: '400px', - height: '400px', - overflowY: 'scroll', - }} - > - - - )} - - ), - icon: - playbookStatus === 'successful' ? ( - - ) : playbookStatus === 'failed' ? ( - - ) : ( - + <> + + {playbookStatus}{' '} + {playbookStatus === 'failed' && ( + + {execRes} + + } + overlayStyle={{ + width: '400px', + height: '400px', + overflowY: 'scroll', + }} + > + + + )} + ), - }, - { - title: 'Docker Connection test', - description: ( - <> - {dockerStatus}{' '} - {dockerStatus === 'failed' && ( - - {dockerConnErrorMessage} - - } - title={'Docker Connection Logs'} - > - - - )} - - ), - icon: - dockerStatus === 'successful' ? ( - - ) : dockerStatus === 'failed' ? ( - - ) : ( - + icon: + playbookStatus === 'successful' ? ( + + ) : playbookStatus === 'failed' ? ( + + ) : ( + + ), + }, + { + title: 'Docker Connection test', + description: ( + <> + {dockerStatus}{' '} + {dockerStatus === 'failed' && ( + + {dockerConnErrorMessage} + + } + title={'Docker Connection Logs'} + > + + + )} + ), - }, - ]} - /> + icon: + dockerStatus === 'successful' ? ( + + ) : dockerStatus === 'failed' ? ( + + ) : ( + + ), + }, + ]} + /> + {smartFailure && ( + + + + Probable cause: {smartFailure.cause} +
+ Probable Resolution: {smartFailure.resolution} +
+ + } + showIcon + type={'error'} + /> +
+ )} + {(playbookStatus === 'failed' || count > 10) && ( + + + + )} + ); }; diff --git a/client/src/components/NewDeviceModal/NewDeviceModal.tsx b/client/src/components/NewDeviceModal/NewDeviceModal.tsx index 05801c71..68afaaf1 100644 --- a/client/src/components/NewDeviceModal/NewDeviceModal.tsx +++ b/client/src/components/NewDeviceModal/NewDeviceModal.tsx @@ -247,6 +247,12 @@ const NewDeviceModal: React.FC = (props) => { }, ]} /> + { + const [showDetails, setShowDetails] = React.useState(false); + + return ( + + + Try switching to the classic Ansible SSH connexion method instead + of paramiko (not available when using a passphrase protected key). + + + SSH → Show advanced → Connection Method → *SSH* + + + [More details] + + + ) : undefined + } + action={ + showDetails ? undefined : ( + + ) + } + showIcon + /> + ); +}; + +export default SwitchConnexionMethod; diff --git a/client/src/components/NoDevice/CarouselNoDevice.tsx b/client/src/components/NoDevice/CarouselNoDevice.tsx new file mode 100644 index 00000000..c3691024 --- /dev/null +++ b/client/src/components/NoDevice/CarouselNoDevice.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import { Carousel, Image } from 'antd'; +import styles from './CarouselWithBlur.less'; + +const CarouselNoDevice = () => ( + +
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+
+
+ +
+
+
+
+); + +export default CarouselNoDevice; diff --git a/client/src/components/NoDevice/CarouselWithBlur.less b/client/src/components/NoDevice/CarouselWithBlur.less new file mode 100644 index 00000000..31ab6cfb --- /dev/null +++ b/client/src/components/NoDevice/CarouselWithBlur.less @@ -0,0 +1,16 @@ +.imageContainer { + position: relative; + overflow: hidden; + border-radius: 10px; + margin-top: auto; + margin-bottom: auto; + + .blurredImage { + display: block; + width: 100%; + height: auto; + margin-top: auto; + margin-bottom: auto; + box-shadow: 0 0 20px 10px rgba(19, 19, 19, 0.5); + } +} diff --git a/client/src/components/NoDevice/NoDeviceModal.tsx b/client/src/components/NoDevice/NoDeviceModal.tsx new file mode 100644 index 00000000..9a04bbe8 --- /dev/null +++ b/client/src/components/NoDevice/NoDeviceModal.tsx @@ -0,0 +1,79 @@ +import NewDeviceModal from '@/components/NewDeviceModal/NewDeviceModal'; +import CarouselNoDevice from '@/components/NoDevice/CarouselNoDevice'; +import TerminalModal, { + TerminalStateProps, +} from '@/components/PlaybookExecutionModal'; +import { Image, Button, Carousel, Modal, Typography } from 'antd'; +import React, { useState } from 'react'; +import { API, SsmAnsible, SsmAgent } from 'ssm-shared-lib'; + +const contentStyle: React.CSSProperties = { + margin: 0, + height: '160px', + color: '#fff', + lineHeight: '160px', + textAlign: 'center', +}; + +const NoDeviceModal = () => { + const [addNewDeviceModalIsOpen, setAddNewDeviceModalIsOpen] = useState(false); + const [isOpen, setIsOpen] = useState(true); + const [terminal, setTerminal] = useState({ + target: undefined, + isOpen: false, + command: undefined, + playbookName: undefined, + mode: SsmAnsible.ExecutionMode.APPLY, + }); + const openOrCloseTerminalModal = (open: boolean) => { + setTerminal({ ...terminal, isOpen: open }); + }; + + const onAddNewDevice = ( + target: API.DeviceItem, + installMethod: SsmAgent.InstallMethods, + ) => { + setTerminal({ + target: [target], + isOpen: true, + quickRef: 'installAgent', + extraVars: [{ extraVar: '_ssm_installMethod', value: installMethod }], + }); + }; + + const handleOk = () => { + setIsOpen(false); + setAddNewDeviceModalIsOpen(true); + }; + + return ( + <> + + + + Add my first device + , + ]} + > + + Thanks for trying Squirrel Servers Manager, we hope you will enjoy + managing your servers with ease! + + + + + ); +}; + +export default NoDeviceModal; diff --git a/server/package.json b/server/package.json index 2321f6b5..e216f801 100644 --- a/server/package.json +++ b/server/package.json @@ -10,7 +10,7 @@ "test": "vitest --disable-console-intercept --reporter=basic ", "coverage": "vitest run --coverage" }, - "version": "0.1.20", + "version": "0.1.21", "author": "Squirrel Team", "dependencies": { "@aws-sdk/client-ecr": "^3.670.0", diff --git a/server/src/core/startup/index.ts b/server/src/core/startup/index.ts index 0688c147..200b2eb6 100644 --- a/server/src/core/startup/index.ts +++ b/server/src/core/startup/index.ts @@ -1,6 +1,7 @@ import { SettingsKeys } from 'ssm-shared-lib'; import { getFromCache, setToCache } from '../../data/cache'; import initRedisValues from '../../data/cache/defaults'; +import { DeviceModel } from '../../data/database/model/Device'; import { PlaybookModel } from '../../data/database/model/Playbook'; import { copyAnsibleCfgFileIfDoesntExist } from '../../helpers/ansible/AnsibleConfigurationHelper'; import PinoLogger from '../../logger'; @@ -48,6 +49,7 @@ class Startup { private async updateScheme() { this.logger.warn(`Scheme version differed, starting writing updates`); await PlaybookModel.syncIndexes(); + await DeviceModel.syncIndexes(); await createADefaultLocalUserRepository(); await initRedisValues(); void setAnsibleVersions(); diff --git a/server/src/data/database/model/Device.ts b/server/src/data/database/model/Device.ts index ab149d9a..fe2759c7 100644 --- a/server/src/data/database/model/Device.ts +++ b/server/src/data/database/model/Device.ts @@ -86,7 +86,7 @@ const schema = new Schema( ip: { type: Schema.Types.String, required: false, - unique: true, + unique: false, }, status: { type: Schema.Types.Number, diff --git a/shared-lib/package.json b/shared-lib/package.json index cb471f37..5988f0b9 100644 --- a/shared-lib/package.json +++ b/shared-lib/package.json @@ -1,6 +1,6 @@ { "name": "ssm-shared-lib", - "version": "0.1.20", + "version": "0.1.21", "description": "", "main": "./distribution/index.js", "author": "Squirrel Team", diff --git a/shared-lib/src/enums/settings.ts b/shared-lib/src/enums/settings.ts index 4994be1f..b128304c 100644 --- a/shared-lib/src/enums/settings.ts +++ b/shared-lib/src/enums/settings.ts @@ -12,7 +12,7 @@ export enum GeneralSettingsKeys { } export enum DefaultValue { - SCHEME_VERSION = '12', + SCHEME_VERSION = '13', SERVER_LOG_RETENTION_IN_DAYS = '30', CONSIDER_DEVICE_OFFLINE_AFTER_IN_MINUTES = '3', CONSIDER_PERFORMANCE_GOOD_MEM_IF_GREATER = '10', diff --git a/site/docs/quickstart.md b/site/docs/quickstart.md index 05355386..53734bce 100644 --- a/site/docs/quickstart.md +++ b/site/docs/quickstart.md @@ -10,8 +10,7 @@ SSM has published versions of the client and server images according to release The `docker-compose.yml` file uses these pre-built images. To use them, you can set up the following Docker Compose file: ### Docker-compose file -```dockerfile -version: '3.8' +```yaml services: proxy: restart: unless-stopped diff --git a/site/docs/technical-guide/manual-install-agent.md b/site/docs/technical-guide/manual-install-agent.md index 4a4aa7d9..8377b773 100644 --- a/site/docs/technical-guide/manual-install-agent.md +++ b/site/docs/technical-guide/manual-install-agent.md @@ -6,6 +6,8 @@ If you have difficulties installing the agent from the UI, you can install it ma [Please read the stack requirements before installing the agent](/docs/requirements) ::: +## NodeJS Vanilla Agent + ### Environment It is possible to customize the behavior of the agent by setting environment variables in the `.env` file: @@ -16,7 +18,7 @@ It is possible to customize the behavior of the agent by setting environment var | `AGENT_HEALTH_CRON_EXPRESSION` | NO | '*/30 * * * * *' | Frequency of agent self-check | | `STATISTICS_CRON_EXPRESSION` | NO | '*/30 * * * * *' | Frequency of stats push | -## Method 1: Installing the Agent with the Provided Shell Script +### Method 1: Installing the Agent with the Provided Shell Script ```shell git clone https://github.com/SquirrelCorporation/SquirrelServersManager-Agent cd ./SquirrelServersManager-Agent @@ -35,7 +37,7 @@ If the device already exists in SSM, use: ``` and replace `DEVICE_ID` with the UUID of the device in SSM (In Inventory, click on the IP and copy the UUID shown in the right drawer). -## Method 2: Building & Installing the Agent Manually +### Method 2: Building & Installing the Agent Manually ```shell git clone https://github.com/SquirrelCorporation/SquirrelServersManager-Agent cd ./SquirrelServersManager-Agent @@ -68,3 +70,57 @@ pm2 start -f ./build/agent.js pm2 startup pm2 save ``` + +## Dockerized Agent + +### Environment +It is possible to customize the behavior of the agent by setting environment variables in the `.env` file: + +| Env | Required | Example | Description | +|---------------------------------|:--------:|:-----------------------:|------------------------------------------------------------| +| `URL_MASTER` | YES | http://192.168.0.3:8000 | URL of the SSM API | +| `OVERRIDE_IP_DETECTION` | NO | 192.168.0.1 | Disable the auto-detection of the IP and set a fixed value | +| `AGENT_HEALTH_CRON_EXPRESSION` | NO | '*/30 * * * * *' | Frequency of agent self-check | +| `STATISTICS_CRON_EXPRESSION` | NO | '*/30 * * * * *' | Frequency of stats push | +| `HOST_ID_PATH` | NO | `/data/` | Path where is stored the registered HostID | +| `LOGS_PATH` | NO | `/data/logs` | Path where are store the logs | +| `HOST_ID` | NO | xxx-xxx-xxx-xxx | UUID of the registered Device in SSM | + +Docker compose: +```yaml +services: + ssm_agent: + image: ghcr.io/squirrelcorporation/squirrelserversmanager-agent:docker + network_mode: host + privileged: true + env_file: + - .env + pid: host + restart: unless-stopped + volumes: + - /proc:/proc + - /var/run/docker.sock:/var/run/docker.sock + - ssm-agent-data:/data + +volumes: + ssm-agent-data: +``` +or +```shell +git clone https://github.com/SquirrelCorporation/SquirrelServersManager-Agent +git checkout docker +docker-compose up -d +``` +or +```shell +docker pull ghcr.io/squirrelcorporation/squirrelserversmanager-agent:docker +docker volume create ssm-agent-data +docker run --network host \ + --privileged \ + --pid=host \ + -v /proc:/proc \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v ssm-agent-data:/data \ + --restart unless-stopped \ + ghcr.io/squirrelcorporation/squirrelserversmanager-agent:docker +``` \ No newline at end of file diff --git a/site/docs/technical-guide/troubleshoot.md b/site/docs/technical-guide/troubleshoot.md index f0408f24..7a52f3d9 100644 --- a/site/docs/technical-guide/troubleshoot.md +++ b/site/docs/technical-guide/troubleshoot.md @@ -1,6 +1,7 @@ # Troubleshooting -## MongoDB / AVX support +## Troubleshoot SSM Installation +### :black_circle: MongoDB / AVX support Recent versions of MongoDB require a CPU with AVX support. If your host CPU doesn't support AVX, try editing the `docker-compose.yml` file and downgrade the image version to the latest known version that doesn't require AVX. ```dockerfile ... @@ -25,33 +26,47 @@ to => command: --quiet ... ``` -## Agent Installation Failed -If SSM is unable to install the agent through Ansible, consider installing the agent manually. -See [Manual Install](/docs/technical-guide/manual-install-agent). +--- + +### :black_circle: Unable to Create or Setup an Admin Account / "JwtStrategy requires a secret or key" +The `.env` file of the `docker-compose.yml` is not set correctly. +See [Quick Start](/docs/quickstart). + -## SSM Does Not Retrieve Services (Docker Containers) +## Troubleshoot Agent Installation +### :yellow_circle: Agent Installation Failed +If SSM is unable to install the agent through Ansible, consider: +- Using an alternative installation method. See [Adding a device](/docs/devices/add-device#_4-installation-method). +- Installing the agent manually. See [Manual Install](/docs/technical-guide/manual-install-agent). + +--- + +### :yellow_circle: Ansible Is Stuck Indefinitely When Applying a Playbook on Turnkey / LXC Container +Try changing the connection method in the device configuration modal from `paramiko` to `ssh`. +![connection-method](/technical-guide/troubleshoot/connection-method.png) + +## Troubleshoot Container/Docker Issues +### :purple_circle: SSM Does Not Retrieve Services (Docker Containers) In some cases, SSM may not be able to retrieve Docker containers from the host. 1. Check that the Docker CLI is available for the SSH user you provided. See the [Official Docker Linux Post-Install](https://docs.docker.com/engine/install/linux-postinstall/) guide. 2. Verify that the Docker socket path is correct. If the problem persists, try using another authentication method. -## SSM Shows "Socket Hangup" in Docker Module Logs +--- + +### :purple_circle: SSM Shows "Socket Hangup" in Docker Module Logs This warning/error is most likely coming from your host's Docker installation. Refer to the [previous point](#ssm-does-not-retrieve-services-docker-containers) for potential solutions. -## The Device's IP Has Automatically Changed to Its LAN IP / The IP Changed to VLAN's One +## Troubleshoot Devices Issues + +### :brown_circle: The Device's IP Has Automatically Changed to Its LAN IP / The IP Changed to VLAN's One To permanently fix the IP on a device: In the `.env` file of the **agent**, add the variable `OVERRIDE_IP_DETECTION=` and restart the agent. See [Manual Install](/docs/technical-guide/manual-install-agent). -## Ansible Is Stuck Indefinitely When Applying a Playbook on Turnkey / LXC Container -Try changing the connection method in the device configuration modal from `paramiko` to `ssh`. -![connection-method](/technical-guide/troubleshoot/connection-method.png) - -## Unable to Create or Setup an Admin Account / "JwtStrategy requires a secret or key" -The `.env` file of the `docker-compose.yml` is not set correctly. -See [Quick Start](/docs/quickstart). +## Misc -## Windows +### :white_circle: Windows Support Windows is not supported (yet)