├── racksdb ├── web │ └── __init__.py ├── dtypes │ ├── __init__.py │ ├── rack_height.py │ ├── netif_type.py │ ├── storage_type.py │ ├── angle.py │ ├── dimension.py │ ├── watts.py │ ├── bits.py │ ├── bytes.py │ └── rack_width.py ├── generic │ ├── __init__.py │ ├── errors.py │ ├── definedtype.py │ └── dumpers │ │ ├── console.py │ │ ├── __init__.py │ │ ├── _common.py │ │ └── json.py ├── tests │ ├── __init__.py │ ├── drawers │ │ ├── __init__.py │ │ ├── test_parameters.py │ │ └── test_axonometric_infrastructure.py │ ├── generic │ │ ├── __init__.py │ │ ├── lib │ │ │ ├── __init__.py │ │ │ ├── bases.py │ │ │ └── views.py │ │ ├── dumpers │ │ │ ├── __init__.py │ │ │ ├── test_factory.py │ │ │ ├── test_dumper_console.py │ │ │ ├── test_dumper_json.py │ │ │ └── test_dumper_yaml.py │ │ ├── test_openapi.py │ │ ├── test_schema_defined_type_loader.py │ │ ├── test_db_list.py │ │ ├── test_views.py │ │ ├── test_schema_file_loader.py │ │ └── test_db_dict.py │ ├── lib │ │ ├── __init__.py │ │ ├── web.py │ │ ├── common.py │ │ └── reference.py │ ├── web │ │ └── __init__.py │ ├── test_defined_type_hexcolor.py │ └── test_defined_type_watts.py ├── drawers │ ├── dtypes │ │ ├── __init__.py │ │ └── hexcolor.py │ ├── coordinates │ │ ├── base.py │ │ ├── json.py │ │ ├── yaml.py │ │ └── __init__.py │ ├── __init__.py │ └── parameters.py ├── version.py └── errors.py ├── docs ├── modules │ ├── misc │ │ ├── nav.adoc │ │ └── pages │ │ │ └── releases.adoc │ ├── install │ │ ├── nav.adoc │ │ └── partials │ │ │ └── pip-deps.adoc │ ├── usage │ │ ├── images │ │ │ ├── raw │ │ │ └── shadowed │ │ ├── examples │ │ │ ├── racksdb.yml │ │ │ ├── custom.yml │ │ │ └── custom.json │ │ ├── nav.adoc │ │ ├── partials │ │ │ └── drawing-deftypes.adoc │ │ └── pages │ │ │ ├── rest.adoc │ │ │ ├── racksdb-web.adoc │ │ │ ├── ui.adoc │ │ │ ├── integrations.adoc │ │ │ └── drawparams.adoc │ ├── overview │ │ ├── nav.adoc │ │ ├── images │ │ │ ├── racksdb_logo.png │ │ │ ├── racksdb_web_ui_screenshots.webp │ │ │ ├── racksdb_diagrams.png │ │ │ ├── racksdb_overview.png │ │ │ └── racksdb_interfaces.png │ │ └── pages │ │ │ ├── start.adoc │ │ │ └── overview.adoc │ └── db │ │ ├── examples │ │ ├── extensions.yml │ │ ├── infrastructure.yml │ │ ├── datacenter.yml │ │ └── types.yml │ │ ├── nav.adoc │ │ ├── pages │ │ ├── structure.adoc │ │ ├── def.adoc │ │ ├── schema.adoc │ │ └── files.adoc │ │ └── partials │ │ ├── symbols.adoc │ │ ├── deftypes.adoc │ │ └── prop-types.adoc ├── utils │ ├── build.yaml │ ├── gen-openapi.py │ ├── schema-objs.py │ ├── drawings-schema-objs.py │ └── schema-objs.adoc.j2 ├── generate-changelog.sh ├── antora.yml ├── man │ ├── racksdb.adoc │ └── racksdb-web.adoc ├── README.md └── update-materials ├── examples ├── extensions │ ├── db │ │ ├── datacenters │ │ ├── types │ │ │ ├── nodes │ │ │ ├── network │ │ │ ├── storage │ │ │ └── racks.yml │ │ ├── infrastructures │ │ └── power.yml │ └── extensions.yml ├── db │ ├── types │ │ ├── misc │ │ │ └── kvm.yml │ │ ├── network │ │ │ └── cisco3650.yml │ │ ├── storage │ │ │ └── qnaph1277.yml │ │ ├── racks.yml │ │ └── nodes │ │ │ ├── sm220bt.yml │ │ │ ├── sm610u.yml │ │ │ ├── hpesyn480.yml │ │ │ ├── bullsx440a5.yml │ │ │ └── dellr550.yml │ ├── infrastructures │ │ ├── sharednet.yml │ │ ├── mercury.yml │ │ └── jupiter.yml │ └── datacenters │ │ ├── paris.yml │ │ └── london.yml └── simple │ └── racksdb.yml ├── frontend ├── env.d.ts ├── public │ ├── favicon.ico │ ├── assets │ │ ├── racksdb_logo.png │ │ └── racksdb-marker.svg │ └── config.json ├── src │ ├── index.css │ ├── App.vue │ ├── main.ts │ ├── views │ │ ├── HomeView.vue │ │ ├── InfrastructuresView.vue │ │ ├── InfrastructureDetailsView.vue │ │ └── DatacentersView.vue │ ├── plugins │ │ ├── http.ts │ │ └── runtimeConfiguration.ts │ ├── components │ │ ├── DatacenterListInfrastructures.vue │ │ ├── HeaderPage.vue │ │ ├── BreadCrumbs.vue │ │ └── HomeViewCards.vue │ └── router │ │ └── index.ts ├── postcss.config.js ├── .prettierrc.json ├── tsconfig.vitest.json ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.app.json ├── index.html ├── .eslintrc.cjs ├── tsconfig.node.json ├── vitest.config.ts ├── vite.config.ts ├── tests │ ├── views │ │ ├── HomeView.spec.ts │ │ ├── InfrastructuresView.spec.ts │ │ ├── InfrastructureDetailsView.spec.ts │ │ ├── DatacenterDetailsView.spec.ts │ │ ├── DatacenterRoomView.spec.ts │ │ └── DatacentersView.spec.ts │ ├── components │ │ ├── HeaderPage.spec.ts │ │ ├── DatacenterListInfrastructures.spec.ts │ │ ├── ComboBox.spec.ts │ │ ├── BreadCrumbs.spec.ts │ │ ├── FiltersBar.spec.ts │ │ ├── HomeViewCards.spec.ts │ │ └── InfrastructureFilters.spec.ts │ └── setup.ts ├── README.md └── package.json ├── assets ├── bitmaps │ ├── favicon.ico │ ├── logo_dark_tiny.png │ ├── logo_dark_large.png │ ├── logo_dark_medium.png │ ├── logo_dark_small.png │ ├── logo_white_large.png │ ├── logo_white_small.png │ ├── logo_white_tiny.png │ ├── logo_colored_large.png │ ├── logo_colored_medium.png │ ├── logo_colored_small.png │ ├── logo_colored_tiny.png │ ├── logo_full_dark_tiny.png │ ├── logo_white_medium.png │ ├── logo_full_dark_large.png │ ├── logo_full_dark_medium.png │ ├── logo_full_dark_small.png │ ├── logo_full_white_large.png │ ├── logo_full_white_small.png │ ├── logo_full_white_tiny.png │ ├── logo_full_colored_large.png │ ├── logo_full_colored_medium.png │ ├── logo_full_colored_small.png │ ├── logo_full_colored_tiny.png │ ├── logo_full_white_medium.png │ ├── logo_full_horizontal_dark_large.png │ ├── logo_full_horizontal_dark_small.png │ ├── logo_full_horizontal_dark_tiny.png │ ├── logo_full_horizontal_white_tiny.png │ ├── logo_full_horizontal_colored_large.png │ ├── logo_full_horizontal_colored_small.png │ ├── logo_full_horizontal_colored_tiny.png │ ├── logo_full_horizontal_dark_medium.png │ ├── logo_full_horizontal_white_large.png │ ├── logo_full_horizontal_white_medium.png │ ├── logo_full_horizontal_white_small.png │ └── logo_full_horizontal_colored_medium.png ├── screenshots │ ├── raw │ │ ├── home.png │ │ ├── racks.png │ │ ├── rooms.png │ │ ├── datacenters.png │ │ ├── infrastructure.png │ │ ├── infrastructures.png │ │ └── infrastructure_filters.png │ ├── shadowed │ │ ├── home.webp │ │ ├── racks.webp │ │ ├── rooms.webp │ │ ├── datacenters.webp │ │ ├── infrastructure.webp │ │ ├── infrastructures.webp │ │ └── infrastructure_filters.webp │ ├── assemblies │ │ └── bitmaps │ │ │ ├── screenshots-small.png │ │ │ └── screenshots-small.webp │ └── build.yaml └── README.md ├── .gitignore ├── .github └── workflows │ └── pre-commit.yml ├── .pre-commit-config.yaml ├── LICENSE └── pyproject.toml /racksdb/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/dtypes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/drawers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/generic/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/web/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/drawers/dtypes/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/generic/lib/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /racksdb/tests/generic/dumpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/modules/misc/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:releases.adoc[] 2 | -------------------------------------------------------------------------------- /docs/modules/install/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:quickstart.adoc[] 2 | -------------------------------------------------------------------------------- /examples/extensions/db/datacenters: -------------------------------------------------------------------------------- 1 | ../../db/datacenters -------------------------------------------------------------------------------- /examples/extensions/db/types/nodes: -------------------------------------------------------------------------------- 1 | ../../../db/types/nodes -------------------------------------------------------------------------------- /docs/modules/usage/images/raw: -------------------------------------------------------------------------------- 1 | ../../../../assets/screenshots/raw -------------------------------------------------------------------------------- /examples/extensions/db/infrastructures: -------------------------------------------------------------------------------- 1 | ../../db/infrastructures/ -------------------------------------------------------------------------------- /examples/extensions/db/types/network: -------------------------------------------------------------------------------- 1 | ../../../db/types/network -------------------------------------------------------------------------------- /examples/extensions/db/types/storage: -------------------------------------------------------------------------------- 1 | ../../../db/types/storage -------------------------------------------------------------------------------- /frontend/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- 1 | ../../assets/bitmaps/favicon.ico -------------------------------------------------------------------------------- /docs/modules/usage/images/shadowed: -------------------------------------------------------------------------------- 1 | ../../../../assets/screenshots/shadowed -------------------------------------------------------------------------------- /examples/db/types/misc/kvm.yml: -------------------------------------------------------------------------------- 1 | model: 19“ KVM Console 2 | height: 1u 3 | -------------------------------------------------------------------------------- /docs/modules/usage/examples/racksdb.yml: -------------------------------------------------------------------------------- 1 | ../../../../examples/simple/racksdb.yml -------------------------------------------------------------------------------- /examples/extensions/db/power.yml: -------------------------------------------------------------------------------- 1 | - capacity: 250kW 2 | - capacity: 1.6MW 3 | -------------------------------------------------------------------------------- /docs/modules/overview/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:start.adoc[] 2 | * xref:overview.adoc[] 3 | -------------------------------------------------------------------------------- /frontend/public/assets/racksdb_logo.png: -------------------------------------------------------------------------------- 1 | ../../../assets/bitmaps/logo_full_white_medium.png -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /docs/modules/overview/images/racksdb_logo.png: -------------------------------------------------------------------------------- 1 | ../../../../assets/bitmaps/logo_full_white_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/favicon.ico -------------------------------------------------------------------------------- /assets/bitmaps/logo_dark_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_dark_tiny.png -------------------------------------------------------------------------------- /assets/screenshots/raw/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/home.png -------------------------------------------------------------------------------- /assets/screenshots/raw/racks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/racks.png -------------------------------------------------------------------------------- /assets/screenshots/raw/rooms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/rooms.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_dark_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_dark_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_dark_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_dark_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_dark_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_dark_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_white_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_white_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_white_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_white_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_white_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_white_tiny.png -------------------------------------------------------------------------------- /frontend/public/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "API_SERVER": "http://localhost:5000/", 3 | "API_VERSION": "v0.6.0" 4 | } 5 | -------------------------------------------------------------------------------- /assets/bitmaps/logo_colored_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_colored_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_colored_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_colored_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_colored_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_colored_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_colored_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_colored_tiny.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_dark_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_dark_tiny.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_white_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_white_medium.png -------------------------------------------------------------------------------- /assets/screenshots/raw/datacenters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/datacenters.png -------------------------------------------------------------------------------- /assets/screenshots/shadowed/home.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/home.webp -------------------------------------------------------------------------------- /assets/screenshots/shadowed/racks.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/racks.webp -------------------------------------------------------------------------------- /assets/screenshots/shadowed/rooms.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/rooms.webp -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_dark_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_dark_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_dark_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_dark_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_dark_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_dark_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_white_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_white_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_white_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_white_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_white_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_white_tiny.png -------------------------------------------------------------------------------- /docs/modules/overview/images/racksdb_web_ui_screenshots.webp: -------------------------------------------------------------------------------- 1 | ../../../../assets/screenshots/assemblies/bitmaps/screenshots-small.webp -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_colored_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_colored_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_colored_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_colored_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_colored_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_colored_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_colored_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_colored_tiny.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_white_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_white_medium.png -------------------------------------------------------------------------------- /assets/screenshots/raw/infrastructure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/infrastructure.png -------------------------------------------------------------------------------- /assets/screenshots/raw/infrastructures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/infrastructures.png -------------------------------------------------------------------------------- /assets/screenshots/shadowed/datacenters.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/datacenters.webp -------------------------------------------------------------------------------- /assets/screenshots/shadowed/infrastructure.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/infrastructure.webp -------------------------------------------------------------------------------- /assets/screenshots/shadowed/infrastructures.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/infrastructures.webp -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_dark_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_dark_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_dark_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_dark_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_dark_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_dark_tiny.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_white_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_white_tiny.png -------------------------------------------------------------------------------- /assets/screenshots/raw/infrastructure_filters.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/raw/infrastructure_filters.png -------------------------------------------------------------------------------- /docs/modules/overview/images/racksdb_diagrams.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/docs/modules/overview/images/racksdb_diagrams.png -------------------------------------------------------------------------------- /docs/modules/overview/images/racksdb_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/docs/modules/overview/images/racksdb_overview.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_colored_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_colored_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_colored_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_colored_small.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_colored_tiny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_colored_tiny.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_dark_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_dark_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_white_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_white_large.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_white_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_white_medium.png -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_white_small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_white_small.png -------------------------------------------------------------------------------- /docs/modules/overview/images/racksdb_interfaces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/docs/modules/overview/images/racksdb_interfaces.png -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | plugins: { 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /assets/bitmaps/logo_full_horizontal_colored_medium.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/bitmaps/logo_full_horizontal_colored_medium.png -------------------------------------------------------------------------------- /assets/screenshots/shadowed/infrastructure_filters.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/shadowed/infrastructure_filters.webp -------------------------------------------------------------------------------- /examples/db/types/network/cisco3650.yml: -------------------------------------------------------------------------------- 1 | model: Cisco Catalyst 3650 switch 2 | height: 1u 3 | netifs: 4 | - type: ethernet 5 | bandwidth: 1Gb 6 | number: 48 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.egg-info/ 3 | .venv/ 4 | build/ 5 | .vite/ 6 | dist/ 7 | /docs/man/*.1 8 | /generated-doc 9 | node_modules/ 10 | .ninja_log 11 | -------------------------------------------------------------------------------- /assets/screenshots/assemblies/bitmaps/screenshots-small.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/assemblies/bitmaps/screenshots-small.png -------------------------------------------------------------------------------- /assets/screenshots/assemblies/bitmaps/screenshots-small.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rackslab/RacksDB/HEAD/assets/screenshots/assemblies/bitmaps/screenshots-small.webp -------------------------------------------------------------------------------- /examples/db/types/storage/qnaph1277.yml: -------------------------------------------------------------------------------- 1 | model: QNAP TS-H1277XU-RP 2 | height: 2u 3 | disks: 4 | - type: disk 5 | size: 4TB 6 | model: Seagate IronWolf 7 | number: 12 8 | -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } 9 | -------------------------------------------------------------------------------- /examples/db/types/racks.yml: -------------------------------------------------------------------------------- 1 | - id: standard 2 | height: 1867mm 3 | width: 600mm 4 | depth: 914mm 5 | slots: 42u 6 | - id: half 7 | height: 1198mm 8 | width: 600mm 9 | depth: 914mm 10 | slots: 24u 11 | -------------------------------------------------------------------------------- /frontend/tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "exclude": [], 4 | "compilerOptions": { 5 | "composite": true, 6 | "lib": [], 7 | "types": ["node", "jsdom"] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/modules/usage/nav.adoc: -------------------------------------------------------------------------------- 1 | * CLI manpages 2 | ** xref:racksdb.adoc[`racksdb`] 3 | ** xref:racksdb-web.adoc[`racksdb-web`] 4 | * xref:drawparams.adoc[] 5 | * xref:lib.adoc[] 6 | * xref:rest.adoc[] 7 | * xref:ui.adoc[] 8 | * xref:integrations.adoc[] 9 | -------------------------------------------------------------------------------- /docs/utils/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Define DPI/sizes for which PNG bitmaps versions of SVG diagrams must be 3 | # generated. 4 | diagrams: 5 | modules/overview/images/racksdb_overview.svg: medium 6 | modules/overview/images/racksdb_interfaces.svg: medium 7 | -------------------------------------------------------------------------------- /frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | /* eslint-env node */ 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], 5 | plugins: [ 6 | require('@tailwindcss/forms'), 7 | ], 8 | } 9 | -------------------------------------------------------------------------------- /examples/extensions/db/types/racks.yml: -------------------------------------------------------------------------------- 1 | - id: standard 2 | height: 1867mm 3 | width: 600mm 4 | depth: 914mm 5 | slots: 42u 6 | weight: 128 7 | - id: half 8 | height: 1198mm 9 | width: 600mm 10 | depth: 914mm 11 | slots: 24u 12 | weight: 64 13 | -------------------------------------------------------------------------------- /racksdb/drawers/coordinates/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2024 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | 8 | class CoordinatesDumper: 9 | def dump(self, coordinates): 10 | raise NotImplementedError 11 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.node.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /docs/modules/usage/examples/custom.yml: -------------------------------------------------------------------------------- 1 | margin: 2 | top: 200 3 | colors: 4 | equipments: 5 | - type: sm610u 6 | background: "#348feb" # blue 7 | - tags: ["hpc", "servers"] 8 | border: "#a66321" # orange 9 | racks: 10 | - type: half 11 | pane: "#8d42f5" # purple 12 | frame: "#81a85e" # green 13 | -------------------------------------------------------------------------------- /examples/extensions/extensions.yml: -------------------------------------------------------------------------------- 1 | _content: 2 | properties: 3 | power: 4 | type: list[:Ups] 5 | optional: true 6 | 7 | _objects: 8 | Ups: 9 | properties: 10 | capacity: 11 | type: ~watts 12 | RackType: 13 | properties: 14 | weight: 15 | type: int 16 | optional: true 17 | -------------------------------------------------------------------------------- /frontend/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "baseUrl": ".", 8 | "paths": { 9 | "@/*": ["./src/*"] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/modules/db/examples/extensions.yml: -------------------------------------------------------------------------------- 1 | _content: 2 | properties: 3 | power: 4 | type: list[:Ups] 5 | optional: true 6 | 7 | _objects: 8 | Ups: 9 | properties: 10 | capacity: 11 | type: ~watts 12 | RackType: 13 | properties: 14 | weight: 15 | type: int 16 | optional: true 17 | -------------------------------------------------------------------------------- /docs/modules/db/nav.adoc: -------------------------------------------------------------------------------- 1 | * xref:def.adoc[] 2 | ** xref:concepts.adoc[] 3 | ** xref:files.adoc[] 4 | ** xref:schema.adoc[] 5 | ** xref:structure.adoc[Structure Reference] 6 | ** xref:ext.adoc[] 7 | ** Positioning How-Tos 8 | *** xref:positioning-racks.adoc[Racks Positioning] 9 | *** xref:positioning-equipments.adoc[Equipments Positioning] 10 | -------------------------------------------------------------------------------- /docs/generate-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | mkdir -p generated-doc/modules/misc/partials 4 | # Replace issue with hyperlinks to github issues 5 | sed 's/\(#\([0-9]\+\)\)/[#\2](https:\/\/github.com\/rackslab\/RacksDB\/issues\/\2\)/g' < CHANGELOG.md | pandoc --from markdown --to asciidoctor --output generated-doc/modules/misc/partials/CHANGELOG.adoc 6 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 16 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: pre-commit 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | pre-commit: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | - uses: actions/setup-python@v3 19 | - uses: pre-commit/action@v3.0.1 20 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | RacksDB 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /racksdb/drawers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from .room import RoomDrawer 8 | from .infrastructure import InfrastructureDrawer 9 | from .axonometric_infrastructure import AxonometricInfrastructureDrawer 10 | 11 | __all__ = ["RoomDrawer", "InfrastructureDrawer", "AxonometricInfrastructureDrawer"] 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/astral-sh/ruff-pre-commit 9 | rev: v0.11.0 10 | hooks: 11 | - id: ruff 12 | types: [python] 13 | args: [ --fix ] 14 | # Run the formatter. 15 | - id: ruff-format 16 | -------------------------------------------------------------------------------- /frontend/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-typescript', 10 | '@vue/eslint-config-prettier/skip-formatting' 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest' 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "nightwatch.conf.*", 8 | "playwright.config.*" 9 | ], 10 | "compilerOptions": { 11 | "composite": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Bundler", 14 | "types": ["node"] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /racksdb/version.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | try: 8 | from importlib import metadata 9 | except ImportError: 10 | # On Python < 3.8, use external backport library importlib-metadata. 11 | import importlib_metadata as metadata 12 | 13 | 14 | def get_version(): 15 | return metadata.version("racksdb") 16 | -------------------------------------------------------------------------------- /assets/screenshots/build.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Define DPI/sizes for which PNG bitmaps version of assemblies must be 3 | # generated. 4 | assemblies: 5 | screenshots.svg: 6 | - small 7 | # List raw screenshots for which versions with dropped shadow must be generated. 8 | shadowed: 9 | - home.png 10 | - datacenters.png 11 | - rooms.png 12 | - racks.png 13 | - infrastructures.png 14 | - infrastructure.png 15 | - infrastructure_filters.png 16 | -------------------------------------------------------------------------------- /docs/modules/misc/pages/releases.adoc: -------------------------------------------------------------------------------- 1 | = Release notes 2 | 3 | This page contains the releases notes of RacksDB with details about the new 4 | features, modifications and bug fixes coming with all releases. 5 | 6 | NOTE: This page is rendered based on the content of 7 | https://github.com/rackslab/racksdb/blob/main/CHANGELOG.md[`CHANGELOG.md`] 8 | in RacksDB source code. 9 | 10 | include::partial$CHANGELOG.adoc[lines=8..-1,leveloffset=-1] 11 | -------------------------------------------------------------------------------- /racksdb/dtypes/rack_height.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeRackHeight(SchemaDefinedType): 11 | pattern = r"(\d+)u" 12 | native = int 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | return int(match.group(1)) 17 | -------------------------------------------------------------------------------- /racksdb/generic/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | 8 | class DBSchemaError(Exception): 9 | pass 10 | 11 | 12 | class DBFormatError(Exception): 13 | pass 14 | 15 | 16 | class DBDumperError(Exception): 17 | pass 18 | 19 | 20 | class DBViewError(Exception): 21 | pass 22 | 23 | 24 | class DBOpenAPIGeneratorError(Exception): 25 | pass 26 | -------------------------------------------------------------------------------- /racksdb/dtypes/netif_type.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeNetifType(SchemaDefinedType): 11 | pattern = r"(ethernet|infiniband)" 12 | native = str 13 | 14 | def parse(self, value): 15 | self._match(value) # just check it matches 16 | return value 17 | -------------------------------------------------------------------------------- /racksdb/dtypes/storage_type.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeStorageType(SchemaDefinedType): 11 | pattern = r"(ssd|disk|nvme)" 12 | native = str 13 | 14 | def parse(self, value): 15 | self._match(value) # just check it matches 16 | return value 17 | -------------------------------------------------------------------------------- /examples/db/types/nodes/sm220bt.yml: -------------------------------------------------------------------------------- 1 | model: SuperMicro A+ Server 2124BT-HTR 2 | height: 1u 3 | width: 1/2 4 | specs: https://www.supermicro.com/en/aplus/system/2u/2124/as-2124bt-htr.cfm 5 | cpu: 6 | sockets: 2 7 | model: AMD EPYC 7573X 8 | specs: https://www.amd.com/en/products/cpu/amd-epyc-7573x 9 | cores: 32 10 | ram: 11 | dimm: 8 12 | size: 32GB 13 | storage: 14 | - type: nvme 15 | model: Samsung 980 Pro 16 | size: 256GB 17 | netifs: 18 | - type: ethernet 19 | bandwidth: 10Gb 20 | -------------------------------------------------------------------------------- /frontend/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'node:url' 2 | import { mergeConfig, defineConfig, configDefaults } from 'vitest/config' 3 | import viteConfig from './vite.config' 4 | 5 | export default mergeConfig( 6 | viteConfig, 7 | defineConfig({ 8 | test: { 9 | environment: 'jsdom', 10 | setupFiles: ['./tests/setup.ts'], 11 | exclude: [...configDefaults.exclude, 'e2e/*'], 12 | root: fileURLToPath(new URL('./', import.meta.url)) 13 | } 14 | }) 15 | ) 16 | -------------------------------------------------------------------------------- /racksdb/drawers/coordinates/json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2024 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import json 8 | 9 | from .base import CoordinatesDumper 10 | 11 | 12 | class CoordinatesDumperJson(CoordinatesDumper): 13 | def __init__(self): 14 | pass 15 | 16 | def dump(self, coordinates): 17 | return json.dumps( 18 | {key: coordinate._serialized for key, coordinate in coordinates.items()} 19 | ) 20 | -------------------------------------------------------------------------------- /racksdb/drawers/coordinates/yaml.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2024 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import yaml 8 | 9 | from .base import CoordinatesDumper 10 | 11 | 12 | class CoordinatesDumperYaml(CoordinatesDumper): 13 | def __init__(self): 14 | pass 15 | 16 | def dump(self, coordinates): 17 | return yaml.dump( 18 | {key: coordinate._serialized for key, coordinate in coordinates.items()} 19 | ) 20 | -------------------------------------------------------------------------------- /docs/antora.yml: -------------------------------------------------------------------------------- 1 | name: racksdb 2 | title: RacksDB 3 | version: v0.6 4 | start_page: overview:start.adoc 5 | asciidoc: 6 | attributes: 7 | source-language: asciidoc@ 8 | table-caption: false 9 | api-version: v0.6.0 10 | nav: 11 | - modules/overview/nav.adoc 12 | - modules/install/nav.adoc 13 | - modules/db/nav.adoc 14 | - modules/usage/nav.adoc 15 | - modules/misc/nav.adoc 16 | ext: 17 | collector: 18 | run: 19 | command: sh docs/generate-changelog.sh 20 | scan: 21 | dir: generated-doc 22 | -------------------------------------------------------------------------------- /frontend/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | vue(), 10 | ], 11 | resolve: { 12 | alias: { 13 | '@': fileURLToPath(new URL('./src', import.meta.url)) 14 | } 15 | }, 16 | build: { 17 | target: 'esnext' // required for top-level await used by runtimeConfiguration plugin 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /examples/db/types/nodes/sm610u.yml: -------------------------------------------------------------------------------- 1 | model: SuperMicro Ultra SYS-610U-TNR 2 | height: 1u 3 | width: full 4 | specs: https://www.supermicro.com/en/products/system/ultra/1u/sys-610u-tnr 5 | cpu: 6 | sockets: 2 7 | model: Intel Xeon Gold 6338 8 | specs: https://ark.intel.com/content/www/us/en/ark/products/212285/intel-xeon-gold-6338-processor-48m-cache-2-00-ghz.html 9 | cores: 20 10 | ram: 11 | dimm: 16 12 | size: 16GB 13 | netifs: 14 | - type: ethernet 15 | bandwidth: 10Gb 16 | - type: ethernet 17 | bandwidth: 10Gb 18 | -------------------------------------------------------------------------------- /racksdb/tests/generic/lib/bases.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | 8 | class TestAppleCrateBase: 9 | def _filter(self, quantity_min=None): 10 | if quantity_min and quantity_min > self.quantity: 11 | return False 12 | return True 13 | 14 | 15 | class TestAppleBase: 16 | def _filter(self, color=None): 17 | if color and color != self.color: 18 | return False 19 | return True 20 | -------------------------------------------------------------------------------- /docs/modules/install/partials/pip-deps.adoc: -------------------------------------------------------------------------------- 1 | [source,console] 2 | ---- 3 | $ sudo apt install libcairo2-dev libgirepository-2.0-dev 4 | ---- 5 | 6 | [CAUTION] 7 | ==== 8 | On Debian _« bookworm »_, `libgirepository-2.0-dev` package is not 9 | available. As a workaround, you can install `libgirepository1.0-dev` instead 10 | with an older version of PyGObject in your virtual environment: 11 | 12 | [source,console] 13 | ---- 14 | $ sudo apt install libcairo2-dev libgirepository1.0-dev 15 | $ pip install "PyGObject<3.43.0" 16 | ---- 17 | ==== 18 | -------------------------------------------------------------------------------- /docs/man/racksdb.adoc: -------------------------------------------------------------------------------- 1 | = racksdb(1) 2 | Rackslab: https://rackslab.io 3 | :doctype: manpage 4 | :manmanual: racksdb 5 | :man-linkstyle: pass:[blue R < >] 6 | 7 | == Name 8 | 9 | racksdb - Explore Yaml database of datacenter infrastructures 10 | 11 | include::../modules/usage/pages/racksdb.adoc[] 12 | 13 | == Resources 14 | 15 | RacksDB web site: https://github.com/rackslab/racksdb 16 | 17 | == Copying 18 | 19 | Copyright (C) 2022-2023 {author}. + 20 | 21 | RacksDB is distributed under the terms of the GNU General Public License v3.0 22 | or later (GPLv3+). 23 | -------------------------------------------------------------------------------- /docs/modules/db/pages/structure.adoc: -------------------------------------------------------------------------------- 1 | = Database Structure Reference 2 | 3 | This page contains *the reference documentation of RacksDB database structure*, 4 | with all objects models and properties. The document starts with a legend of 5 | some symbols used for schematic representation of properties. Then, the root 6 | content of the database is described, followed by all subsequents objects. At 7 | the end, the defined types are described. 8 | 9 | include::partial$symbols.adoc[] 10 | 11 | include::partial$objects.adoc[] 12 | 13 | include::partial$deftypes.adoc[] 14 | -------------------------------------------------------------------------------- /racksdb/drawers/coordinates/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2024 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from .json import CoordinatesDumperJson 8 | from .yaml import CoordinatesDumperYaml 9 | 10 | 11 | class CoordinateDumperFactory: 12 | FORMATS = {"json": CoordinatesDumperJson, "yaml": CoordinatesDumperYaml} 13 | 14 | @staticmethod 15 | def create(format: str): 16 | assert format in CoordinateDumperFactory.FORMATS.keys() 17 | return CoordinateDumperFactory.FORMATS[format]() 18 | -------------------------------------------------------------------------------- /docs/man/racksdb-web.adoc: -------------------------------------------------------------------------------- 1 | = racksdb-web(1) 2 | Rackslab: https://rackslab.io 3 | :doctype: manpage 4 | :manmanual: racksdb-web 5 | :man-linkstyle: pass:[blue R < >] 6 | 7 | == Name 8 | 9 | racksdb-web - RacksDB web application with REST API 10 | 11 | include::../modules/usage/pages/racksdb-web.adoc[] 12 | 13 | == Resources 14 | 15 | RacksDB web site: https://github.com/rackslab/racksdb 16 | 17 | == Copying 18 | 19 | Copyright (C) 2022-2023 {author}. + 20 | 21 | RacksDB is distributed under the terms of the GNU General Public License v3.0 22 | or later (GPLv3+). 23 | -------------------------------------------------------------------------------- /examples/db/types/nodes/hpesyn480.yml: -------------------------------------------------------------------------------- 1 | model: HPE Synergy 480 Gen10 Compute Module 2 | height: 5u 3 | width: 1/7 4 | specs: https://www.hpe.com/psnow/doc/PSN1010025863USEN.pdf 5 | cpu: 6 | sockets: 2 7 | model: Intel Xeon Scalable 6148 8 | specs: https://www.intel.com/content/www/us/en/products/sku/120489/intel-xeon-gold-6148-processor-27-5m-cache-2-40-ghz/specifications.html 9 | cores: 20 10 | ram: 11 | dimm: 4 12 | size: 32GB 13 | storage: 14 | - type: nvme 15 | model: Intel Optane 16 | size: 256GB 17 | netifs: 18 | - type: ethernet 19 | bandwidth: 10Gb 20 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_openapi.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.openapi import OpenAPIGenerator 10 | 11 | from .lib.views import TestDBViews 12 | from .lib.common import valid_schema 13 | 14 | 15 | class TestDBViewSet(unittest.TestCase): 16 | def test_generator(self): 17 | generator = OpenAPIGenerator( 18 | "Test", "1.0", {"Test": valid_schema()}, TestDBViews() 19 | ) 20 | generator.generate() 21 | -------------------------------------------------------------------------------- /frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2022-2023 Rackslab 2 | 3 | This file is part of RacksDB. 4 | 5 | SPDX-License-Identifier: MIT */ 6 | 7 | import './index.css' 8 | import { createApp } from 'vue' 9 | 10 | import App from './App.vue' 11 | import router from './router' 12 | import { initRuntimeConfiguration, runtimeConfiguration } from './plugins/runtimeConfiguration' 13 | import { httpPlugin } from './plugins/http' 14 | 15 | const app = createApp(App) 16 | app.use(router) 17 | app.use(runtimeConfiguration, await initRuntimeConfiguration()) 18 | app.use(httpPlugin) 19 | app.mount('#app') 20 | -------------------------------------------------------------------------------- /docs/modules/usage/examples/custom.json: -------------------------------------------------------------------------------- 1 | { 2 | "margin": { 3 | "top": 200 4 | }, 5 | "colors": { 6 | "equipments": [ 7 | { 8 | "type": "sm610u", 9 | "background": "#348feb" 10 | }, 11 | { 12 | "tags": ["hpc", "servers"], 13 | "border": "#a66321" 14 | } 15 | ], 16 | "racks": [ 17 | { 18 | "type": "half", 19 | "pane": "#8d42f5", 20 | "frame": "#81a85e" 21 | } 22 | ] 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /racksdb/errors.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | 8 | from .generic.errors import DBFormatError, DBSchemaError 9 | 10 | 11 | class RacksDBError(Exception): 12 | pass 13 | 14 | 15 | class RacksDBRequestError(RacksDBError): 16 | pass 17 | 18 | 19 | class RacksDBNotFoundError(RacksDBError): 20 | pass 21 | 22 | 23 | class RacksDBFormatError(DBFormatError): 24 | pass 25 | 26 | 27 | class RacksDBSchemaError(DBSchemaError): 28 | pass 29 | 30 | 31 | class RacksDBDrawingError(RacksDBError): 32 | pass 33 | -------------------------------------------------------------------------------- /examples/db/types/nodes/bullsx440a5.yml: -------------------------------------------------------------------------------- 1 | model: BullSequana X410 A5 2 | height: 2u 3 | width: full 4 | specs: https://atos.net/wp-content/uploads/2023/04/BullSequana-X400-series_Factsheet.pdf 5 | cpu: 6 | sockets: 2 7 | model: AMD EPYC 7773X 8 | specs: https://www.amd.com/en/products/cpu/amd-epyc-7773x 9 | cores: 64 10 | ram: 11 | dimm: 16 12 | size: 32GB 13 | gpu: 14 | - model: A100 15 | memory: 40GB 16 | - model: A100 17 | memory: 40GB 18 | - model: A100 19 | memory: 40GB 20 | - model: A100 21 | memory: 40GB 22 | netifs: 23 | - type: ethernet 24 | bandwidth: 10Gb 25 | - type: ethernet 26 | bandwidth: 10Gb 27 | -------------------------------------------------------------------------------- /examples/db/types/nodes/dellr550.yml: -------------------------------------------------------------------------------- 1 | model: Dell PowerEdge R550 2 | height: 2u 3 | width: full 4 | specs: https://www.delltechnologies.com/asset/en-us/products/servers/technical-support/dellemc-poweredge-r550-spec-sheet.pdf 5 | cpu: 6 | sockets: 2 7 | model: Intel Xeon Silver 4316 8 | specs: https://www.intel.com/content/www/us/en/products/sku/215270/intel-xeon-silver-4316-processor-30m-cache-2-30-ghz/specifications.html 9 | cores: 20 10 | ram: 11 | dimm: 4 12 | size: 32GB 13 | storage: 14 | - type: ssd 15 | size: 960GB 16 | netifs: 17 | - type: ethernet 18 | bandwidth: 1Gb 19 | - type: ethernet 20 | bandwidth: 1Gb 21 | -------------------------------------------------------------------------------- /racksdb/dtypes/angle.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | from racksdb.generic.errors import DBFormatError 9 | 10 | 11 | class SchemaDefinedTypeAngle(SchemaDefinedType): 12 | pattern = r"\d+" 13 | native = int 14 | 15 | def parse(self, value): 16 | match = self._match(value) 17 | degrees = int(match.group(0)) 18 | if degrees < 0 or degrees > 360: 19 | raise DBFormatError(f"Invalid angle of {degrees} degrees") 20 | return degrees 21 | -------------------------------------------------------------------------------- /racksdb/dtypes/dimension.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeDimension(SchemaDefinedType): 11 | pattern = r"(\d+(.\d+)?)(mm|cm|m)" 12 | native = int 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | size = float(match.group(1)) 17 | unit = match.group(3) 18 | if unit == "cm": 19 | size *= 10 20 | elif unit == "m": 21 | size *= 1000 22 | return int(size) 23 | -------------------------------------------------------------------------------- /racksdb/tests/lib/web.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import json 8 | 9 | import flask 10 | 11 | 12 | class RacksDBCustomTestResponse(flask.Response): 13 | """Custom flask Response class to backport text property of 14 | werkzeug.test.TestResponse class on werkzeug < 0.15.""" 15 | 16 | @property 17 | def text(self): 18 | return self.get_data(as_text=True) 19 | 20 | @property 21 | def json(self): 22 | if self.mimetype != "application/json": 23 | return None 24 | return json.loads(self.text) 25 | -------------------------------------------------------------------------------- /racksdb/dtypes/watts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeWatts(SchemaDefinedType): 11 | pattern = r"(\d+(.\d+)?)(W|kW|MW)" 12 | native = int 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | quantity = float(match.group(1)) 17 | unit = match.group(3) 18 | if unit == "kW": 19 | quantity *= 10**3 20 | elif unit == "MW": 21 | quantity *= 10**6 22 | return int(quantity) 23 | -------------------------------------------------------------------------------- /racksdb/generic/definedtype.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import re 8 | 9 | from .errors import DBFormatError 10 | 11 | 12 | class SchemaDefinedType: 13 | def __init__(self): 14 | self.name = self.__class__.__module__ 15 | 16 | def __str__(self): 17 | return f"~{self.name}" 18 | 19 | def _match(self, value): 20 | regex = re.compile(self.pattern) 21 | match = regex.match(str(value)) 22 | if match is None: 23 | raise DBFormatError(f"Unable to match {self} pattern with value {value}") 24 | return match 25 | -------------------------------------------------------------------------------- /frontend/tests/views/HomeView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | 12 | import HomeView from '@/views/HomeView.vue' 13 | 14 | describe('HomeView', () => { 15 | test('renders Overview title and stubs children', () => { 16 | const wrapper = mount(HomeView, { 17 | global: { 18 | stubs: { 19 | BreadCrumbs: true, 20 | HomeViewCards: true 21 | } 22 | } 23 | }) 24 | 25 | expect(wrapper.text()).toContain('Overview') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /frontend/src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 | 24 | -------------------------------------------------------------------------------- /frontend/src/plugins/http.ts: -------------------------------------------------------------------------------- 1 | /*Copyright (c) 2022-2023 Rackslab 2 | 3 | This file is part of RacksDB. 4 | 5 | SPDX-License-Identifier: MIT */ 6 | 7 | import type { App, Plugin } from 'vue' 8 | import { inject } from 'vue' 9 | import axios from 'axios' 10 | import type { AxiosInstance } from 'axios' 11 | 12 | const injectionKey = Symbol('http') 13 | 14 | export const useHttp = () => inject(injectionKey) as AxiosInstance 15 | 16 | export const httpPlugin: Plugin = { 17 | install(app: App) { 18 | const http = axios.create({ 19 | baseURL: `${app.config.globalProperties.$rc.api_server}${app.config.globalProperties.$rc.api_version}` 20 | }) 21 | app.provide(injectionKey, http) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /racksdb/dtypes/bits.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeBits(SchemaDefinedType): 11 | pattern = r"(\d+(.\d+)?)(Tb|Gb|Mb)" 12 | native = int 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | quantity = float(match.group(1)) 17 | unit = match.group(3) 18 | if unit == "Mb": 19 | quantity *= 10**6 20 | elif unit == "Gb": 21 | quantity *= 10**9 22 | elif unit == "Tb": 23 | quantity *= 10**12 24 | return int(quantity) 25 | -------------------------------------------------------------------------------- /frontend/public/assets/racksdb-marker.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /racksdb/dtypes/bytes.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeBytes(SchemaDefinedType): 11 | pattern = r"(\d+(.\d+)?)(TB|GB|MB)" 12 | native = int 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | quantity = float(match.group(1)) 17 | unit = match.group(3) 18 | if unit == "MB": 19 | quantity *= 1024**2 20 | elif unit == "GB": 21 | quantity *= 1024**3 22 | elif unit == "TB": 23 | quantity *= 1024**4 24 | return int(quantity) 25 | -------------------------------------------------------------------------------- /examples/db/infrastructures/sharednet.yml: -------------------------------------------------------------------------------- 1 | description: Central network infrastructure 2 | layout: 3 | - rack: R2-A02 4 | network: 5 | - name: netmercurysw[01-02] 6 | type: cisco3650 7 | slot: 18 8 | tags: [network] 9 | - rack: rackE01 10 | network: 11 | - name: netsw01 12 | type: cisco3650 13 | slot: 40 14 | tags: [network, shared] 15 | - rack: rackE02 16 | network: 17 | - name: netsw02 18 | type: cisco3650 19 | slot: 40 20 | tags: [network, shared] 21 | - rack: R5-A01 22 | network: 23 | - name: netsw03 24 | type: cisco3650 25 | slot: 40 26 | tags: [network, shared] 27 | - rack: R5-A02 28 | network: 29 | - name: netsw04 30 | type: cisco3650 31 | slot: 40 32 | tags: [network, shared] 33 | -------------------------------------------------------------------------------- /racksdb/dtypes/rack_width.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from racksdb.generic.definedtype import SchemaDefinedType 8 | 9 | 10 | class SchemaDefinedTypeRackWidth(SchemaDefinedType): 11 | pattern = r"full|(\d+)(/\d+)?" 12 | native = float 13 | 14 | def parse(self, value): 15 | match = self._match(value) 16 | if value == "full": 17 | return 1.0 18 | else: 19 | dividend = int(match.group(1)) 20 | divisor = match.group(2) 21 | if divisor is not None: 22 | divisor = float(divisor[1:]) 23 | else: 24 | divisor = 1.0 25 | return dividend / divisor 26 | -------------------------------------------------------------------------------- /racksdb/drawers/dtypes/hexcolor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import re 8 | from typing import Tuple 9 | 10 | from racksdb.generic.definedtype import SchemaDefinedType 11 | 12 | 13 | class SchemaDefinedTypeHexcolor(SchemaDefinedType): 14 | pattern = r"^#[0-9a-fA-F]{6}([0-9a-fA-F]{2})?$" 15 | native = Tuple[float, float, float, float] 16 | 17 | def parse(self, value): 18 | self._match(value) 19 | hexrgb = re.findall("[0-9a-fA-F]{2}", value) 20 | # If optional alpha channel is not defined, set it to 255/ff (ie. fully opaque) 21 | if len(hexrgb) == 3: 22 | hexrgb.append("ff") 23 | rgb = tuple(int(f"0x{_hex}", 16) / 255 for _hex in hexrgb) 24 | return rgb 25 | -------------------------------------------------------------------------------- /docs/modules/usage/partials/drawing-deftypes.adoc: -------------------------------------------------------------------------------- 1 | == Defined Types 2 | 3 | :url-deftypes: https://github.com/rackslab/racksdb/blob/main/racksdb/drawers/dtypes 4 | 5 | [cols="2a,2a,1l,3a"] 6 | |=== 7 | |Name|Examples|Resulting type|Comment 8 | 9 | |[#deftype-hexcolor]`~hexcolor` 10 | {url-deftypes}/hexcolor.py[icon:code[]] 11 | |* `#ffffff` 12 | * `#a66321` 13 | * `#348febaa` 14 | |tuple(float, float, float, float) 15 | |Hexadecimal color code (see 16 | https://en.wikipedia.org/wiki/Web_colors#Hex_triplet[Wikipedia article for more 17 | details]) with optional alpha channel. Default alpha channel value is `ff` (ie. 18 | fully opaque). The resulting value is tuple of 4 floats between 0 and 1 19 | representing the rate of red, green and blue primary colors in the resulting 20 | color and the color opacity (from 0 for fully transparent to 1 for fully 21 | opaque). 22 | 23 | |=== 24 | -------------------------------------------------------------------------------- /racksdb/generic/dumpers/console.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from typing import Any 8 | 9 | from ClusterShell.NodeSet import NodeSet 10 | 11 | from ..errors import DBDumperError 12 | 13 | 14 | class DBDumperConsole: 15 | def __init__(self, show_types=False, objects_map={}, fold=True): 16 | self.objects_map = objects_map 17 | self.fold = fold 18 | 19 | def dump( 20 | self, 21 | obj: Any, 22 | ) -> str: 23 | if not isinstance(obj, list): 24 | raise DBDumperError(f"Unsupported type '{type(obj)}' for DBDumperConsole") 25 | if self.fold: 26 | nodeset = NodeSet() 27 | for item in obj: 28 | nodeset.update(item) 29 | return str(nodeset) 30 | else: 31 | return "\n".join(obj) 32 | -------------------------------------------------------------------------------- /frontend/src/components/DatacenterListInfrastructures.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 32 | -------------------------------------------------------------------------------- /docs/modules/db/pages/def.adoc: -------------------------------------------------------------------------------- 1 | = Database Definition 2 | 3 | The content of RacksDB database is formatted in https://yaml.org/[YAML 4 | language]. The database can be stored in a single YAML file or splitted in 5 | multiple files. The content of the files must match the database schema provided 6 | by RacksDB with its expected objects and properties. 7 | 8 | This section gives all detailed information to define a RacksDB database, 9 | including: 10 | 11 | * The xref:files.adoc[location of the database] on the system and how-to 12 | organize it into multiple files. 13 | * General information of the xref:schema.adoc[schema file], 14 | * The xref:structure.adoc[database structure], with its objects and the involved 15 | defined types. 16 | * How-to xref:ext.adoc[define extensions] of database schema for custom data. 17 | * Guides to help specify the xref:positioning-racks.adoc[positions of racks] 18 | and xref:positioning-equipments.adoc[equipments in racks] 19 | -------------------------------------------------------------------------------- /racksdb/tests/test_defined_type_hexcolor.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.drawers.dtypes.hexcolor import SchemaDefinedTypeHexcolor 10 | from racksdb.generic.errors import DBFormatError 11 | 12 | 13 | class TestDefinedTypeHexcolor(unittest.TestCase): 14 | def test_defined_type_hexcolor(self): 15 | defined_type = SchemaDefinedTypeHexcolor() 16 | self.assertEqual(defined_type.parse("#004080"), (0, 64 / 255, 128 / 255, 1.0)) 17 | self.assertEqual( 18 | defined_type.parse("#ffb4c5"), (1.0, 180 / 255, 197 / 255, 1.0) 19 | ) 20 | 21 | def test_defined_type_hexcolor_invalid_values(self): 22 | defined_type = SchemaDefinedTypeHexcolor() 23 | for value in ["001122", "fail"]: 24 | with self.assertRaises(DBFormatError): 25 | defined_type.parse(value) 26 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_schema_defined_type_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2022-2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.schema import SchemaDefinedTypeLoader 10 | from racksdb.generic.definedtype import SchemaDefinedType 11 | 12 | 13 | class TestSchemaDefinedTypeLoader(unittest.TestCase): 14 | def test_schema_defined_type_loader(self): 15 | # Load defined types provided in RacksDB 16 | loader = SchemaDefinedTypeLoader("racksdb.dtypes") 17 | # Verify at least one defined type has been loaded 18 | self.assertGreater(len(loader.content), 0) 19 | # Verify loader content is a dict 20 | self.assertIs(type(loader.content), dict) 21 | # Verify content values are valid SchemaDefinedType 22 | for defined_type in loader.content.values(): 23 | self.assertIsInstance(defined_type, SchemaDefinedType) 24 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # RacksDB Documentation 2 | 3 | ## Generate manpage 4 | 5 | To generate the manpage, run this command: 6 | 7 | ```sh 8 | $ ./update-materials man 9 | ``` 10 | 11 | ## Generate objects reference documentation 12 | 13 | To generate the schema objects partial of database structure reference 14 | documentation, run this command: 15 | 16 | ```sh 17 | $ ./update-materials schema-ref 18 | ``` 19 | 20 | ## Generate objects reference documentation 21 | 22 | To generate the schema objects partial of drawing parameters reference 23 | documentation, run this command: 24 | 25 | ```sh 26 | $ ./update-materials drawing-ref 27 | ``` 28 | 29 | ## Update OpenAPI description 30 | 31 | To update reference OpenAPI description of RacksDB REST API, run this command: 32 | 33 | ```sh 34 | $ ./update-materials openapi 35 | ``` 36 | 37 | ## Update PNG exports of SVG diagrams 38 | 39 | To update PNG bitmaps exports of SVG diagrams, run this command: 40 | 41 | ```sh 42 | $ ./update-materials png 43 | ``` 44 | -------------------------------------------------------------------------------- /racksdb/tests/test_defined_type_watts.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.dtypes.watts import SchemaDefinedTypeWatts 10 | from racksdb.generic.errors import DBFormatError 11 | 12 | 13 | class TestDefinedTypeWatts(unittest.TestCase): 14 | def test_defined_type_watts(self): 15 | defined_type = SchemaDefinedTypeWatts() 16 | self.assertEqual(defined_type.parse("15W"), 15) 17 | self.assertEqual(defined_type.parse("100.4kW"), 100.4 * 10**3) 18 | self.assertEqual(defined_type.parse("3.4MW"), 3.4 * 10**6) 19 | self.assertEqual(defined_type.parse("1.1W"), 1) # rounded to integer below 20 | 21 | def test_defined_type_watts_invalid_values(self): 22 | defined_type = SchemaDefinedTypeWatts() 23 | for value in ["1", "1.4.3W", "3.4K"]: 24 | with self.assertRaises(DBFormatError): 25 | defined_type.parse(value) 26 | -------------------------------------------------------------------------------- /frontend/src/views/InfrastructuresView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /racksdb/generic/dumpers/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from .yaml import DBDumperYAML, SchemaDumperYAML 8 | from .json import DBDumperJSON 9 | from .console import DBDumperConsole 10 | from ..errors import DBDumperError 11 | 12 | 13 | class DBDumperFactory: 14 | FORMATS = {"yaml": DBDumperYAML, "json": DBDumperJSON, "console": DBDumperConsole} 15 | 16 | @staticmethod 17 | def get(_format): 18 | if _format not in DBDumperFactory.FORMATS: 19 | raise DBDumperError(f"Unsupported DB dump format {_format}") 20 | return DBDumperFactory.FORMATS[_format] 21 | 22 | 23 | class SchemaDumperFactory: 24 | FORMATS = {"yaml": SchemaDumperYAML} 25 | 26 | @staticmethod 27 | def get(_format): 28 | if _format not in SchemaDumperFactory.FORMATS: 29 | raise DBDumperError(f"Unsupported schema dump format {_format}") 30 | return SchemaDumperFactory.FORMATS[_format] 31 | -------------------------------------------------------------------------------- /examples/db/infrastructures/mercury.yml: -------------------------------------------------------------------------------- 1 | description: Mercury HPC cluster 2 | tags: 3 | - hpc 4 | - cluster 5 | layout: 6 | - rack: R1-A01 7 | tags: [compute] 8 | nodes: 9 | - name: mecn[0001-0040] 10 | type: sm220bt 11 | slot: 1 12 | - name: mecn0200 13 | type: sm610u 14 | slot: 22 15 | - name: mecn[0041-0060] 16 | type: sm610u 17 | slot: 23 18 | - rack: R1-A02 19 | tags: [compute] 20 | nodes: 21 | - name: mecn[0061-0116] 22 | type: hpesyn480 23 | slot: 3 24 | - rack: R2-A02 25 | network: 26 | - name: mesw[01-04] 27 | type: cisco3650 28 | slot: 9 29 | tags: [network] 30 | storage: 31 | - name: swnas01 32 | type: qnaph1277 33 | slot: 13 34 | tags: [storage] 35 | - rack: R2-A03 36 | nodes: 37 | - name: mesrv[0001-0004] 38 | type: dellr550 39 | slot: 9 40 | tags: [servers] 41 | - name: megpu[0001-0008] 42 | type: bullsx440a5 43 | slot: 21 44 | tags: [ia, gpu] 45 | misc: 46 | - name: mekvm01 47 | type: kvm 48 | slot: 18 49 | -------------------------------------------------------------------------------- /docs/modules/db/examples/infrastructure.yml: -------------------------------------------------------------------------------- 1 | # tag::name[] 2 | name: {infra} 3 | # end::name[] 4 | # tag::desc[] 5 | description: Tiger cluster 6 | # end::desc[] 7 | # tag::layout[] 8 | layout: 9 | # end::layout[] 10 | # tag::rack-1[] 11 | - rack: R01 # 1^st^ rack 12 | nodes: 13 | - name: cn[001-040] # *set 1*: 40 *denses* nodes starting from slot *2* 14 | type: dense 15 | slot: 3 16 | - name: cn[100-104] # *set 2*: 4 *classic* nodes starting from slot *24* 17 | type: classic 18 | slot: 25 19 | - name: srv[001-002] # *set 3*: 2 *fat* nodes starting from slot *30* 20 | type: fat 21 | slot: 31 22 | # end::rack-1[] 23 | # tag::rack-2[] 24 | - rack: R02 # 2^nd^ rack 25 | nodes: 26 | - name: cn[201-228] # *set 4*: 28 *blade* nodes starting from slot *2* 27 | type: blade 28 | slot: 3 29 | network: 30 | - name: sw[001-004] # *set 5*: 4 rack switches starting from slot *26* 31 | type: racksw 32 | slot: 27 33 | storage: 34 | - name: nas001 # *set 6*: 1 NAS in slot *35* 35 | type: nas 36 | slot: 36 37 | # end::rack-2[] 38 | -------------------------------------------------------------------------------- /frontend/src/plugins/runtimeConfiguration.ts: -------------------------------------------------------------------------------- 1 | /*Copyright (c) 2022-2023 Rackslab 2 | 3 | This file is part of RacksDB. 4 | 5 | SPDX-License-Identifier: MIT */ 6 | 7 | import type { App, InjectionKey, Plugin } from 'vue' 8 | import { inject } from 'vue' 9 | 10 | export interface RuntimeConfiguration { 11 | api_server: string 12 | api_version: string 13 | } 14 | 15 | export const injectionKey = Symbol('rc') as InjectionKey 16 | 17 | export const runtimeConfiguration: Plugin = { 18 | install: (app: App, configuration: RuntimeConfiguration) => { 19 | app.provide(injectionKey, configuration) 20 | app.config.globalProperties.$rc = configuration 21 | } 22 | } 23 | 24 | export const initRuntimeConfiguration = async (): Promise => { 25 | const resp = await fetch('/config.json') 26 | const value = await resp.json() 27 | 28 | return { 29 | api_server: value.API_SERVER, 30 | api_version: value.API_VERSION 31 | } as RuntimeConfiguration 32 | } 33 | 34 | export const loadRuntimeConfiguration = () => inject(injectionKey) as RuntimeConfiguration 35 | -------------------------------------------------------------------------------- /assets/README.md: -------------------------------------------------------------------------------- 1 | # RacksDB assets 2 | 3 | All assets are generated based on the content of `branding.svg` and 4 | `screenshots/raw/` by running this command: 5 | 6 | ```sh 7 | $ ./update-assets 8 | ``` 9 | 10 | This automatically generates: 11 | 12 | - Scalable SVG files of the logo in `scalables/` folder, 13 | - Bitmaps PNG files (in various sizes) of the logo in `bitmaps/` folder, 14 | - Favicon in `bitmaps/` folder, 15 | - Shadowed and Webp compressed versions of raw PNG screenshots. 16 | 17 | The script has some requirements: 18 | 19 | * Python3 20 | * [Ninja build system](https://ninja-build.org/). On Debian/Ubuntu, this can be 21 | installed with: 22 | 23 | ```sh 24 | $ sudo apt install ninja-build 25 | ``` 26 | * [Inkscape](https://inkscape.org/) 27 | * [RFL build package](https://github.com/rackslab/RFL/tree/main/src/build) 28 | * [Scour Python package](https://github.com/scour-project/scour/tree/master) 29 | * [ImageMagick](https://imagemagick.org/index.php) 30 | 31 | All the generated files are commited and pushed in Git repository to allow 32 | direct usage by documentation site, main `README.md` file, etc… 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022-2025 Rackslab 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /racksdb/generic/dumpers/_common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from typing import Dict, Union 8 | 9 | 10 | class MapperDumper: 11 | def __init__(self, objects_map: Dict[str, Union[str, None]]): 12 | self.objects_map = objects_map 13 | 14 | def map(self, obj, prop, value): 15 | found = False 16 | if f"{obj.__class__.__name__}.{prop}" in self.objects_map.keys(): 17 | target = self.objects_map[f"{obj.__class__.__name__}.{prop}"] 18 | found = True 19 | if value.__class__.__name__ in self.objects_map.keys(): 20 | target = self.objects_map[value.__class__.__name__] 21 | found = True 22 | if found: 23 | # If the object is mapped to None, discard the attribute and 24 | # continue to the next one. 25 | if target is None: 26 | return None 27 | # Else, map the object to one of its attribute. 28 | return getattr(value, target) 29 | # Mapping not found, return value unmodified. 30 | return value 31 | -------------------------------------------------------------------------------- /docs/modules/overview/pages/start.adoc: -------------------------------------------------------------------------------- 1 | = Getting Started 2 | 3 | image::overview:racksdb_logo.png[RacksDB Overview,150,float=right] 4 | 5 | *RacksDB* is an _open source_ solution for modeling your datacenters 6 | infrastructures in a simple database schema to store information about 7 | the equipments in your datacenters provided with tools and library to request 8 | this database. 9 | 10 | icon:glasses[] Get a xref:overview.adoc[complete overview] of RacksDB purpose, 11 | goals and characteristics. 12 | 13 | icon:download[] For installation procedure, checkout the 14 | xref:install:quickstart.adoc[quickstart guide]. 15 | 16 | Advanced detailed informations are available: 17 | 18 | [no-bullet] 19 | * icon:database[] xref:db:def.adoc[Database schema reference documentation] 20 | * icon:terminal[] xref:usage:racksdb.adoc[`racksdb` command manpage] 21 | * icon:code[] xref:usage:lib.adoc[Python Library] and 22 | xref:usage:rest.adoc[REST API] reference documentations 23 | * icon:mouse-pointer[] xref:usage:ui.adoc[Web UI user guide] 24 | * icon:maximize[] xref:db:ext.adoc[Schema extension howto] 25 | 26 | image::overview:racksdb_web_ui_screenshots.webp[RacksDB Web UI] 27 | -------------------------------------------------------------------------------- /frontend/src/components/HeaderPage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 38 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_db_list.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.db import DBList 10 | 11 | from .lib.common import valid_db 12 | 13 | 14 | class TestDBList(unittest.TestCase): 15 | def setUp(self): 16 | self.db = valid_db() 17 | self.assertIsInstance(self.db.stock.content, DBList) 18 | 19 | def test_iter(self): 20 | nb = 0 21 | for item in self.db.stock.content: 22 | nb += 1 23 | self.assertEqual(nb, 20) 24 | 25 | def test_len(self): 26 | self.assertEqual(len(self.db.stock.content), 20) 27 | 28 | def test_itervalues(self): 29 | nb = 0 30 | for item in self.db.stock.content.itervalues(): 31 | nb += 1 32 | self.assertEqual(nb, 2) 33 | 34 | def test_filter(self): 35 | selected = self.db.stock.content.filter(quantity_min=15) 36 | self.assertIsInstance(selected, DBList) 37 | self.assertEqual(len(selected), 10) 38 | selected = self.db.stock.content.filter(quantity_min=5) 39 | self.assertEqual(len(selected), 20) 40 | -------------------------------------------------------------------------------- /examples/db/infrastructures/jupiter.yml: -------------------------------------------------------------------------------- 1 | description: Jupiter data cluster 2 | tags: 3 | - data 4 | - cluster 5 | layout: 6 | - rack: rackE01 7 | storage: 8 | - name: junas[01-04] 9 | type: qnaph1277 10 | slot: 4 11 | tags: [storage] 12 | network: 13 | - name: jusw01 14 | type: cisco3650 15 | slot: 38 16 | tags: [network] 17 | - rack: rackE02 18 | tags: [storage] 19 | storage: 20 | - name: junas[05-08] 21 | type: qnaph1277 22 | slot: 4 23 | tags: [storage] 24 | network: 25 | - name: jusw02 26 | type: cisco3650 27 | slot: 38 28 | tags: [network] 29 | - rack: R5-A01 30 | storage: 31 | - name: junas[09-12] 32 | type: qnaph1277 33 | slot: 4 34 | tags: [storage] 35 | misc: 36 | - name: jukvm01 37 | type: kvm 38 | slot: 18 39 | nodes: 40 | - name: jusrv1 41 | type: dellr550 42 | slot: 19 43 | network: 44 | - name: jusw03 45 | type: cisco3650 46 | slot: 38 47 | tags: [network] 48 | - rack: R5-A02 49 | storage: 50 | - name: junas[13-16] 51 | type: qnaph1277 52 | slot: 4 53 | tags: [storage] 54 | network: 55 | - name: jusw04 56 | type: cisco3650 57 | slot: 38 58 | tags: [network] 59 | -------------------------------------------------------------------------------- /docs/modules/db/pages/schema.adoc: -------------------------------------------------------------------------------- 1 | = Schema 2 | 3 | RacksDB is provided with a database schema. It is basically a YAML file that 4 | defines the _objects_ represented in the database with their _properties_, and 5 | the relations between these objects. 6 | 7 | The content of this file eventually defines the 8 | xref:structure.adoc[database structure]. This schema can be customized to store 9 | additional data using xref:ext.adoc[schema extensions]. 10 | 11 | == Path 12 | 13 | When RacksDB is installed, the schema is available in path 14 | [.path]#`/usr/share/racksdb/schemas/racksdb.yml`#. 15 | 16 | The file is also available in 17 | https://github.com/rackslab/racksdb/blob/main/schemas/racksdb.yml[source code Git 18 | repository]. 19 | 20 | == Content 21 | 22 | The schema file is a mapping that contains 3 main keys: 23 | 24 | `_version`:: Version number of the schema as a string. 25 | `_content`:: Database root object definition. 26 | `_objects`:: All other objects definitions. 27 | 28 | The format of `_content` is an xref:#object[object definition]. The format of 29 | `_objects` is a mapping whose keys are objects names and values are 30 | xref:#object[objects definitions]. 31 | 32 | include::partial$prop-types.adoc[] 33 | -------------------------------------------------------------------------------- /frontend/tests/views/InfrastructuresView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount, flushPromises } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | import InfrastructuresView from '@/views/InfrastructuresView.vue' 14 | 15 | const infrastructuresMock = vi.fn() 16 | 17 | vi.mock('@/plugins/http', () => ({ 18 | useHttp: () => ({}) 19 | })) 20 | 21 | vi.mock('@/composables/RacksDBAPI', () => ({ 22 | useRacksDBAPI: () => ({ 23 | infrastructures: infrastructuresMock 24 | }) 25 | })) 26 | 27 | describe('InfrastructuresView', () => { 28 | beforeEach(() => vi.clearAllMocks()) 29 | 30 | test('loads infrastructures and passes them to ComboBox', async () => { 31 | infrastructuresMock.mockResolvedValueOnce([{ name: 'core' }, { name: 'edge' }]) 32 | 33 | const wrapper = mount(InfrastructuresView, { 34 | global: { stubs: { BreadCrumbs: true, ComboBox: true } } 35 | }) 36 | 37 | await flushPromises() 38 | await nextTick() 39 | 40 | expect(infrastructuresMock).toHaveBeenCalled() 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_views.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.views import DBView, DBAction 10 | from racksdb.generic.errors import DBViewError 11 | 12 | from .lib.views import TestDBViews 13 | 14 | 15 | class TestDBViewSet(unittest.TestCase): 16 | def setUp(self): 17 | self.views = TestDBViews() 18 | 19 | def test_iter(self): 20 | for view in self.views: 21 | self.assertIsInstance(view, DBView) 22 | 23 | def test_views_actions(self): 24 | actions = self.views.views_actions() 25 | self.assertIsInstance(actions, list) 26 | for action in actions: 27 | self.assertIsInstance(action, DBAction) 28 | 29 | def test_actions(self): 30 | for action in self.views.actions(): 31 | self.assertIsInstance(action, DBAction) 32 | 33 | def test_getitem(self): 34 | _ = self.views["apples"] 35 | 36 | def test_getitem_not_found(self): 37 | with self.assertRaisesRegex( 38 | DBViewError, "^Unable to find view for 'fail' content$" 39 | ): 40 | _ = self.views["fail"] 41 | -------------------------------------------------------------------------------- /docs/modules/usage/pages/rest.adoc: -------------------------------------------------------------------------------- 1 | = REST API 2 | 3 | RacksDB provides a REST API to request database content and draw diagrams of 4 | datacenter rooms and infrastructures racks. This REST API is served by 5 | xref:racksdb-web.adoc[`racksdb-web` command]. This page contains the reference 6 | documentation of this API. 7 | 8 | [subs=attributes] 9 | ++++ 10 | 11 | 18 | 34 | ++++ 35 | -------------------------------------------------------------------------------- /frontend/tests/components/HeaderPage.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | import { getRouter } from 'vue-router-mock' 12 | 13 | import HeaderPage from '@/components/HeaderPage.vue' 14 | 15 | describe('HeaderPage', () => { 16 | test('renders navigation links', () => { 17 | const router = getRouter() 18 | router.currentRoute.value = { 19 | meta: { entry: 'home' } 20 | } 21 | 22 | const wrapper = mount(HeaderPage) 23 | 24 | expect(wrapper.text()).toContain('Home') 25 | expect(wrapper.text()).toContain('Datacenters') 26 | expect(wrapper.text()).toContain('Infrastructures') 27 | }) 28 | 29 | test('highlights active route based on meta entry', () => { 30 | const router = getRouter() 31 | router.currentRoute.value = { 32 | meta: { entry: 'datacenters' } 33 | } 34 | 35 | const wrapper = mount(HeaderPage) 36 | 37 | // Check that the component renders the navigation links 38 | expect(wrapper.text()).toContain('Home') 39 | expect(wrapper.text()).toContain('Datacenters') 40 | expect(wrapper.text()).toContain('Infrastructures') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /docs/modules/db/partials/symbols.adoc: -------------------------------------------------------------------------------- 1 | :symbol-seq: icon:list[title=Sequence] 2 | :symbol-ref: icon:arrow-up-right-from-square[title=Reference] 3 | :symbol-obj: icon:cube[title=Object] 4 | :symbol-deftype: icon:shapes[title=Defined type] 5 | :symbol-backref: icon:share-from-square[title=Reference,flip=horizontal] 6 | :symbol-key: icon:key[title=Key] 7 | :symbol-computed: icon:cogs[title=Computed] 8 | 9 | == Symbols legend 10 | 11 | Some icons are used in the tables of this page for symbolic representations of 12 | xref:db:concepts.adoc#advanced[advanced data types]: 13 | 14 | [no-bullet] 15 | - {symbol-ref} xref:db:concepts.adoc#reference[*reference*] 16 | - {symbol-obj} xref:db:concepts.adoc#object[*object*] 17 | - {symbol-deftype} xref:db:concepts.adoc#deftype[*defined types*] 18 | - {symbol-backref} xref:db:concepts.adoc#backref[*back reference*] 19 | 20 | Other symbols are used for xref:db:concepts.adoc#attributes[attributes] of 21 | properties: 22 | 23 | [no-bullet] 24 | - {symbol-seq} xref:db:concepts.adoc#sequence[*sequence*] 25 | - {symbol-key} xref:db:concepts.adoc#key[*key*] 26 | - {symbol-computed} xref:db:concepts.adoc#computed[*computed*] 27 | 28 | The optional/required attribute is indicated in the _Required_ column of 29 | properties definition table. When defined, the default value attribute is 30 | mentioned in the _Description_ column. 31 | -------------------------------------------------------------------------------- /racksdb/tests/generic/dumpers/test_factory.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.dumpers import DBDumperFactory, SchemaDumperFactory 10 | from racksdb.generic.dumpers.json import DBDumperJSON 11 | from racksdb.generic.dumpers.yaml import DBDumperYAML, SchemaDumperYAML 12 | from racksdb.generic.dumpers.console import DBDumperConsole 13 | from racksdb.generic.errors import DBDumperError 14 | 15 | 16 | class TestDBDumperFactory(unittest.TestCase): 17 | def test_get(self): 18 | self.assertIs(DBDumperFactory.get("json"), DBDumperJSON) 19 | self.assertIs(DBDumperFactory.get("yaml"), DBDumperYAML) 20 | self.assertIs(DBDumperFactory.get("console"), DBDumperConsole) 21 | 22 | def test_get_fail(self): 23 | with self.assertRaisesRegex(DBDumperError, "Unsupported DB dump format fail"): 24 | DBDumperFactory.get("fail") 25 | 26 | 27 | class TestSchemaDumperFactor(unittest.TestCase): 28 | def test_get(self): 29 | self.assertIs(SchemaDumperFactory.get("yaml"), SchemaDumperYAML) 30 | 31 | def test_get_fail(self): 32 | with self.assertRaisesRegex( 33 | DBDumperError, "Unsupported schema dump format fail" 34 | ): 35 | SchemaDumperFactory.get("fail") 36 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # ui 2 | 3 | ui provides a web interface for simple and efficient access to the database data. 4 | 5 | ## VSCode/IDE 6 | For a better use, we recommend to use [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) 7 | To make the TypeScript language service aware of `.vue` type we recommend to use [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin). 8 | To enhance the Tailwind code we recommend to use Tailwind CSS [IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss) 9 | 10 | ## Customize configuration 11 | 12 | See [Vite Configuration Reference](https://vitejs.dev/config/). 13 | 14 | ## Project Setup 15 | 16 | ```sh 17 | npm install 18 | ``` 19 | 20 | ### Compile and Hot-Reload for Development 21 | 22 | ```sh 23 | npm run dev 24 | ``` 25 | 26 | ### Type-Check, Compile and Minify for Production 27 | 28 | ```sh 29 | npm run build 30 | ``` 31 | 32 | ### Run Unit Tests with [Vitest](https://vitest.dev/) 33 | 34 | ```sh 35 | npm run test:unit 36 | ``` 37 | 38 | ### Lint with [ESLint](https://eslint.org/) 39 | 40 | ```sh 41 | npm run lint 42 | ``` 43 | 44 | ### Prettier formatting with [Prettier](https://prettier.io/) 45 | 46 | ```sh 47 | npm run format 48 | ``` 49 | 50 | ### Type-Check for Typescript 51 | 52 | ```sh 53 | npm run type-check 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/utils/gen-openapi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | from pathlib import Path 5 | 6 | from rfl.build.projectversion import project_version 7 | from racksdb import RacksDB 8 | from racksdb.views import RacksDBViews 9 | from racksdb.generic.schema import Schema, SchemaFileLoader, SchemaDefinedTypeLoader 10 | from racksdb.generic.openapi import OpenAPIGenerator 11 | from racksdb.generic.dumpers import DBDumperFactory 12 | from racksdb.drawers.parameters import DrawingParameters 13 | 14 | 15 | def main(): 16 | current_dir = os.path.dirname(os.path.realpath(__file__)) 17 | racksdb_schema_path = Path(current_dir).joinpath("../../schemas/racksdb.yml") 18 | racksdb_schema = Schema( 19 | SchemaFileLoader(racksdb_schema_path), 20 | SchemaDefinedTypeLoader(RacksDB.DEFINED_TYPES_MODULE), 21 | ) 22 | drawings_schema_path = Path(current_dir).joinpath("../../schemas/drawings.yml") 23 | drawing_schema = Schema( 24 | SchemaFileLoader(drawings_schema_path), 25 | SchemaDefinedTypeLoader(DrawingParameters.DEFINED_TYPES_MODULE), 26 | ) 27 | views = RacksDBViews() 28 | openapi = OpenAPIGenerator( 29 | "RacksDB", 30 | project_version(), 31 | {"RacksDB": racksdb_schema, "Drawings": drawing_schema}, 32 | views, 33 | ) 34 | dumper = DBDumperFactory.get("yaml")() 35 | print(dumper.dump(openapi.generate())) 36 | 37 | 38 | if __name__ == "__main__": 39 | main() 40 | -------------------------------------------------------------------------------- /frontend/tests/components/DatacenterListInfrastructures.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | 12 | import DatacenterListInfrastructures from '@/components/DatacenterListInfrastructures.vue' 13 | 14 | describe('DatacenterListInfrastructures', () => { 15 | test('renders infrastructure links when infrastructures are provided', () => { 16 | const wrapper = mount(DatacenterListInfrastructures, { 17 | props: { 18 | infrastructures: ['core', 'edge', 'storage'] 19 | } 20 | }) 21 | 22 | expect(wrapper.text()).toContain('core') 23 | expect(wrapper.text()).toContain('edge') 24 | expect(wrapper.text()).toContain('storage') 25 | expect(wrapper.text()).toContain(',') // comma separators 26 | }) 27 | 28 | test('renders dash when no infrastructures are provided', () => { 29 | const wrapper = mount(DatacenterListInfrastructures, { 30 | props: { 31 | infrastructures: [] 32 | } 33 | }) 34 | 35 | expect(wrapper.text()).toContain('-') 36 | }) 37 | 38 | test('renders single infrastructure without comma', () => { 39 | const wrapper = mount(DatacenterListInfrastructures, { 40 | props: { 41 | infrastructures: ['core'] 42 | } 43 | }) 44 | 45 | expect(wrapper.text()).toContain('core') 46 | expect(wrapper.text()).not.toContain(',') 47 | }) 48 | }) 49 | -------------------------------------------------------------------------------- /docs/modules/db/examples/datacenter.yml: -------------------------------------------------------------------------------- 1 | # tag::datacenter[] 2 | datacenters: 3 | - name: mydatacenter 4 | rooms: 5 | - name: myroom 6 | # end::datacenter[] 7 | # tag::dimensions[] 8 | dimensions: 9 | depth: 8m 10 | width: 10m 11 | # end::dimensions[] 12 | # tag::rows[] 13 | rows: 14 | # end::rows[] 15 | # tag::row-r1[] 16 | # Row R1 with 4 racks horizontally aligned (no rotation) 17 | - name: R1 18 | position: 19 | depth: 2m 20 | width: 1m 21 | racks: 22 | - name: R1-[01-04] # 4 racks 23 | type: standard 24 | # end::row-r1[] 25 | 26 | # tag::row-r2[] 27 | # Row R2 with 4 racks and doors oriented to the top on the room. 28 | - name: R2 29 | reversed: true 30 | position: 31 | depth: 1.5m 32 | width: 6m 33 | racks: 34 | - name: R2-[01-04] # 4 racks 35 | type: standard 36 | # end::row-r2[] 37 | 38 | # tag::row-r3[] 39 | # Row R3 with 6 racks 40 | - name: R3 41 | position: 42 | depth: 4m 43 | width: 2m 44 | rotation: 30 # Slight rotation 45 | racks: 46 | - name: R3-[01-06] # 6 racks 47 | type: standard 48 | # end::row-r3[] 49 | 50 | # tag::row-r4[] 51 | # Row R4 with 6 racks. 52 | - name: R4 53 | reversed: true # Get doors oriented to room's right wall 54 | position: 55 | depth: 3m 56 | width: 8m 57 | rotation: 90 # Vertical row 58 | racks: 59 | - name: R4-[01-04] # 4 racks 60 | type: standard 61 | # end::row-r4[] 62 | -------------------------------------------------------------------------------- /racksdb/drawers/parameters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from pathlib import Path 8 | from typing import Union 9 | 10 | from ..generic.schema import Schema, SchemaFileLoader, SchemaDefinedTypeLoader 11 | from ..generic.db import GenericDB 12 | 13 | 14 | class DrawingParameters(GenericDB): 15 | DEFAULT_SCHEMA = "/usr/share/racksdb/schemas/drawings.yml" 16 | PREFIX = "DrawingParameters" 17 | DEFINED_TYPES_MODULE = "racksdb.drawers.dtypes" 18 | 19 | def __init__(self, schema, loader): 20 | super().__init__(self.PREFIX, schema) 21 | self._loader = loader 22 | 23 | @classmethod 24 | def load( 25 | cls, 26 | db_loader, 27 | schema: Union[str, Path, None] = None, 28 | ): 29 | # Unfortunately, default values to arguments cannot be used as they are 30 | # class attributes and the class is not defined yet at this stage at 31 | # compilation time. As an alternative, the value None is checked at 32 | # runtime and replaced by values of class attributes. 33 | 34 | if schema is None: 35 | schema = Path(cls.DEFAULT_SCHEMA) 36 | elif isinstance(schema, str): 37 | schema = Path(schema) 38 | 39 | _schema = Schema( 40 | SchemaFileLoader(schema), 41 | SchemaDefinedTypeLoader(cls.DEFINED_TYPES_MODULE), 42 | ) 43 | _db = cls(_schema, db_loader) 44 | super(cls, _db).load(_db._loader) 45 | return _db 46 | -------------------------------------------------------------------------------- /racksdb/tests/generic/dumpers/test_dumper_console.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.dumpers.console import DBDumperConsole 10 | from racksdb.generic.db import DBList, DBDict 11 | from racksdb.generic.errors import DBDumperError 12 | 13 | 14 | class TestDBDumperConsole(unittest.TestCase): 15 | def test_dump(self): 16 | dumper = DBDumperConsole() 17 | # DBDumperConsole only support lists 18 | content = DBList() 19 | content.append("foo") 20 | content.append("bar") 21 | result = dumper.dump(content) 22 | self.assertEqual(result, "bar,foo") 23 | 24 | def test_dump_unsupported(self): 25 | dumper = DBDumperConsole() 26 | content = DBDict() 27 | content["foo"] = "bar" 28 | with self.assertRaisesRegex( 29 | DBDumperError, 30 | "Unsupported type '' for " 31 | "DBDumperConsole", 32 | ): 33 | dumper.dump(content) 34 | 35 | def test_dump_fold(self): 36 | dumper = DBDumperConsole() 37 | content = DBList() 38 | content.append("foo1") 39 | content.append("foo2") 40 | result = dumper.dump(content) 41 | self.assertEqual(result, "foo[1-2]") 42 | 43 | def test_dump_expand(self): 44 | dumper = DBDumperConsole(fold=False) 45 | content = DBList() 46 | content.append("foo1") 47 | content.append("foo2") 48 | result = dumper.dump(content) 49 | self.assertEqual(result, "foo1\nfoo2") 50 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "racksdb-webui", 3 | "version": "0.6.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "run-p type-check \"build-only {@}\" --", 8 | "preview": "vite preview", 9 | "test:unit": "vitest", 10 | "build-only": "vite build", 11 | "type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false", 12 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", 13 | "format": "prettier --write src/ tests/" 14 | }, 15 | "dependencies": { 16 | "@headlessui/vue": "^1.7.17", 17 | "@heroicons/vue": "^2.1.1", 18 | "axios": "^1.6.2", 19 | "leaflet": "^1.9.4", 20 | "vue": "^3.4", 21 | "vue-router": "^4.2.4" 22 | }, 23 | "devDependencies": { 24 | "@rushstack/eslint-patch": "^1.3.3", 25 | "@tailwindcss/forms": "^0.5.7", 26 | "@tsconfig/node18": "^18.2.2", 27 | "@types/jsdom": "^21.1.3", 28 | "@types/leaflet": "^1.9.8", 29 | "@types/node": "^18.17.17", 30 | "@vitejs/plugin-vue": "^5.2.3", 31 | "@vue/eslint-config-prettier": "^8.0.0", 32 | "@vue/eslint-config-typescript": "^12.0.0", 33 | "@vue/test-utils": "^2.4.1", 34 | "@vue/tsconfig": "^0.4.0", 35 | "autoprefixer": "^10.4.16", 36 | "eslint": "^8.49.0", 37 | "eslint-plugin-vue": "^9.17.0", 38 | "jsdom": "^22.1.0", 39 | "npm-run-all2": "^6.0.6", 40 | "postcss": "^8.4.31", 41 | "prettier": "^3.0.3", 42 | "prettier-plugin-tailwindcss": "^0.5.5", 43 | "tailwindcss": "^3.3.3", 44 | "typescript": "~5.2.0", 45 | "vite": "^6.2.3", 46 | "vitest": "^3.0.9", 47 | "vue-router-mock": "^2.0.0", 48 | "vue-tsc": "^2.0.29" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /docs/modules/db/examples/types.yml: -------------------------------------------------------------------------------- 1 | types: 2 | 3 | # nodes 4 | nodes: 5 | - id: dense # node type for set 1 6 | height: 1u 7 | width: 1/2 # half rack width 8 | # tag::boilerplate[] 9 | model: Dense node of 1U height and half rack width 10 | cpus: 11 | sockets: 2 12 | model: Intel Xeon Silver 4316 13 | cores: 20 14 | ram: 15 | dimm: 4 16 | size: 32GB 17 | # end::boilerplate[] 18 | - id: classic # node type for set 2 19 | height: 1u 20 | width: full 21 | # tag::boilerplate[] 22 | model: Classic node of 1U height 23 | cpus: 24 | sockets: 2 25 | model: Intel Xeon Silver 4316 26 | cores: 20 27 | ram: 28 | dimm: 4 29 | size: 32GB 30 | # end::boilerplate[] 31 | - id: fat # node type for set 3 32 | height: 2u 33 | width: full 34 | # tag::boilerplate[] 35 | model: Large node of 2U height 36 | cpus: 37 | sockets: 2 38 | model: Intel Xeon Silver 4316 39 | cores: 20 40 | ram: 41 | dimm: 4 42 | size: 32GB 43 | # end::boilerplate[] 44 | - id: blade # node type for set 4 45 | height: 5u 46 | width: 1/7 # 7 blades can be installed in the width of the rack 47 | # tag::boilerplate[] 48 | model: Blade node 49 | cpus: 50 | sockets: 2 51 | model: Intel Xeon Silver 4316 52 | cores: 20 53 | ram: 54 | dimm: 4 55 | size: 32GB 56 | # end::boilerplate[] 57 | 58 | # network equipments 59 | network: 60 | - id: racksw # network equipment type for set 5 61 | height: 1u 62 | model: Rack switch 63 | 64 | # storage equipments 65 | storage: 66 | - id: nas # storage equipment type for set 6 67 | height: 2u 68 | model: 2U NAS 69 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "RacksDB" 7 | version = "0.6.0" 8 | description = "Modelize your datacenters infrastructures in YAML database" 9 | license = {text = "MIT"} 10 | requires-python = ">=3.6" 11 | keywords = ["cmdb", "inventory", "yaml", "datacenters", "racks", "hpc", "cluster"] 12 | authors = [ 13 | {name = "Rémi Palancher", email = "remi@rackslab.io"}, 14 | ] 15 | dependencies = [ 16 | "PyYAML", 17 | "ClusterShell", 18 | "pycairo", 19 | "PyGObject", 20 | "RFL.log >= 1.6.0", 21 | ] 22 | readme = "README.md" 23 | classifiers = [ 24 | "Development Status :: 5 - Production/Stable", 25 | "Environment :: Console", 26 | "Intended Audience :: System Administrators", 27 | "Topic :: Software Development :: Libraries :: Python Modules", 28 | "Topic :: Database :: Database Engines/Servers", 29 | "Topic :: System :: Clustering", 30 | "Topic :: System :: Systems Administration", 31 | ] 32 | 33 | [project.optional-dependencies] 34 | dev = [ 35 | "Flask-Cors", 36 | ] 37 | web = [ 38 | "Flask", 39 | "requests-toolbelt", 40 | ] 41 | tests = [ 42 | "coverage", 43 | "parameterized", 44 | "pytest", 45 | "pytest-cov", 46 | ] 47 | 48 | [project.scripts] 49 | racksdb = "racksdb.exec:RacksDBExec.run" 50 | racksdb-web = "racksdb.web.app:RacksDBWebApp.run" 51 | 52 | [tool.setuptools.packages.find] 53 | include = ["racksdb*"] 54 | 55 | [project.urls] 56 | "Homepage" = "https://github.com/rackslab/RacksDB" 57 | "Bug Tracker" = "https://github.com/rackslab/RacksDB/issues" 58 | 59 | [tool.ruff.lint] 60 | # Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. 61 | select = ["E", "F"] 62 | -------------------------------------------------------------------------------- /frontend/tests/setup.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { vi } from 'vitest' 10 | import { createRouterMock, injectRouterMock } from 'vue-router-mock' 11 | import { RouterLinkStub } from '@vue/test-utils' 12 | 13 | // JSDOM may not provide createObjectURL; mock it to prevent console errors 14 | if (typeof URL.createObjectURL !== 'function') { 15 | URL.createObjectURL = vi.fn(() => 'blob:mock-url') 16 | } 17 | 18 | if (typeof URL.revokeObjectURL !== 'function') { 19 | URL.revokeObjectURL = vi.fn() 20 | } 21 | 22 | // Mock ResizeObserver for Headless UI components 23 | global.ResizeObserver = vi.fn().mockImplementation(() => ({ 24 | observe: vi.fn(), 25 | unobserve: vi.fn(), 26 | disconnect: vi.fn() 27 | })) 28 | 29 | // Create router mock with Vitest spy 30 | const router = createRouterMock({ 31 | spy: { 32 | create: vi.fn, 33 | reset: vi.clearAllMocks 34 | }, 35 | // Add any default routes here 36 | routes: [ 37 | { path: '/', name: 'home' }, 38 | { path: '/datacenters', name: 'datacenters' }, 39 | { path: '/datacenters/:name', name: 'datacenterdetails' }, 40 | { path: '/datacenters/:datacenterName/:datacenterRoom', name: 'datacenterroom' }, 41 | { path: '/infrastructures', name: 'infrastructures' }, 42 | { path: '/infrastructures/:name', name: 'infrastructuredetails' } 43 | ] 44 | }) 45 | 46 | // Inject router mock globally 47 | injectRouterMock(router) 48 | 49 | // Make router available globally for tests 50 | global.routerMock = router 51 | 52 | // Configure global stubs for Vue Test Utils 53 | import { config } from '@vue/test-utils' 54 | 55 | config.global.stubs = { 56 | RouterLink: RouterLinkStub 57 | } 58 | -------------------------------------------------------------------------------- /frontend/tests/views/InfrastructureDetailsView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount, flushPromises } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | import InfrastructureDetailsView from '@/views/InfrastructureDetailsView.vue' 14 | 15 | const infrastructuresMock = vi.fn() 16 | const infrastructureImageSvgMock = vi.fn() 17 | 18 | vi.mock('@/plugins/http', () => ({ 19 | useHttp: () => ({}) 20 | })) 21 | 22 | vi.mock('@/composables/RacksDBAPI', () => ({ 23 | useRacksDBAPI: () => ({ 24 | infrastructures: infrastructuresMock, 25 | infrastructureImageSvg: infrastructureImageSvgMock 26 | }) 27 | })) 28 | 29 | describe('InfrastructureDetailsView', () => { 30 | beforeEach(() => { 31 | vi.clearAllMocks() 32 | }) 33 | 34 | test('loads infrastructure details and image blob', async () => { 35 | infrastructuresMock.mockResolvedValueOnce([ 36 | { name: 'core', layout: [], racks: [], dimensions: { width: 0, depth: 0 } } 37 | ]) 38 | infrastructureImageSvgMock.mockResolvedValueOnce( 39 | new Blob([''], { type: 'image/svg+xml' }) 40 | ) 41 | 42 | const wrapper = mount(InfrastructureDetailsView, { 43 | props: { name: 'core' }, 44 | global: { 45 | stubs: { BreadCrumbs: true, Dialog: true, DialogPanel: true, InfrastructureTable: true } 46 | }, 47 | attachTo: document.body 48 | }) 49 | 50 | await flushPromises() 51 | await nextTick() 52 | 53 | expect(infrastructuresMock).toHaveBeenCalled() 54 | expect(infrastructureImageSvgMock).toHaveBeenCalledWith('core') 55 | }) 56 | }) 57 | -------------------------------------------------------------------------------- /examples/db/datacenters/paris.yml: -------------------------------------------------------------------------------- 1 | tags: 2 | - freecooling 3 | - tier2 4 | rooms: 5 | - name: noisy 6 | dimensions: 7 | width: 14m 8 | depth: 25m 9 | rows: 10 | - name: R1 11 | position: 12 | width: 3m 13 | depth: 4m 14 | racks: 15 | - name: R1-A[01-10] 16 | type: standard 17 | tags: 18 | - first 19 | - name: R2 20 | reversed: true 21 | position: 22 | width: 3m 23 | depth: 6m 24 | racks: 25 | - name: R2-A[01-02] 26 | type: half 27 | - name: R2-A[03-10] 28 | type: standard 29 | slot: 2 30 | - name: R3 31 | position: 32 | width: 3m 33 | depth: 9m 34 | racks: 35 | - name: R3-A[01-05] 36 | type: standard 37 | slot: 2 38 | - name: R3-A[07-10] 39 | type: standard 40 | slot: 9 41 | - name: R4 42 | reversed: True 43 | position: 44 | width: 3m 45 | depth: 12m 46 | rotation: 90 47 | racks: 48 | - name: R4-A[01-06] 49 | type: standard 50 | - name: R5 51 | position: 52 | width: 5m 53 | depth: 12m 54 | rotation: 90 55 | racks: 56 | - name: R5-A[01-06] 57 | type: standard 58 | - name: R6 59 | position: 60 | width: 10m 61 | depth: 15m 62 | rotation: 60 63 | racks: 64 | - name: R6-A[01-06] 65 | type: standard 66 | - name: R7 67 | position: 68 | width: 11.8m 69 | depth: 20m 70 | rotation: 120 71 | racks: 72 | - name: R7-A[01-06] 73 | type: standard 74 | tags: 75 | - last 76 | location: 77 | longitude: 2.294694 78 | latitude: 48.858093 79 | -------------------------------------------------------------------------------- /examples/simple/racksdb.yml: -------------------------------------------------------------------------------- 1 | types: 2 | nodes: 3 | - id: sm220bt 4 | model: SuperMicro A+ 2124BT-HTR 5 | height: 1u 6 | width: 1/2 7 | cpu: 8 | cores: 32 9 | model: AMD EPYC 7573X 10 | sockets: 2 11 | ram: 12 | dimm: 8 13 | size: 32GB 14 | storage: 15 | - model: Samsung 980 Pro 16 | size: 256GB 17 | type: nvme 18 | netifs: 19 | - bandwidth: 10Gb 20 | type: ethernet 21 | network: 22 | - id: cisco3650 23 | model: Cisco Catalyst 3650 switch 24 | height: 1u 25 | netifs: 26 | - bandwidth: 1Gb 27 | number: 48 28 | type: ethernet 29 | storage: 30 | - id: qnaph1277 31 | model: QNAP TS-H1277XU-RP 32 | height: 2u 33 | disks: 34 | - model: Seagate IronWolf 35 | number: 12 36 | size: 4TB 37 | type: disk 38 | racks: 39 | - id: standard 40 | height: 1867mm 41 | width: 600mm 42 | depth: 914mm 43 | slots: 42u 44 | 45 | datacenters: 46 | - name: paris 47 | rooms: 48 | - name: atlas 49 | dimensions: 50 | depth: 4m 51 | width: 8m 52 | rows: 53 | - name: R1 54 | position: 55 | depth: 1.5m 56 | width: 1.5m 57 | racks: 58 | - name: R1-A[01-10] 59 | type: standard 60 | 61 | infrastructures: 62 | - name: tiger 63 | description: HPC cluster 64 | layout: 65 | - rack: R1-A01 66 | tags: 67 | - compute 68 | nodes: 69 | - name: ticn[0001-0040] 70 | slot: 1 71 | type: sm220bt 72 | - rack: R1-A02 73 | tags: 74 | - compute 75 | network: 76 | - type: cisco3650 77 | name: tisw[01-02] 78 | slot: 10 79 | tags: 80 | - network 81 | storage: 82 | - name: tinas01 83 | slot: 14 84 | tags: 85 | - storage 86 | type: qnaph1277 87 | -------------------------------------------------------------------------------- /racksdb/tests/lib/common.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import os 8 | from pathlib import Path 9 | 10 | CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) 11 | 12 | 13 | def _first_path(paths, error): 14 | for path in paths: 15 | if path.exists(): 16 | return path 17 | raise FileNotFoundError(error) 18 | 19 | 20 | def drawing_schema_path(): 21 | return _first_path( 22 | [ 23 | Path(CURRENT_DIR).joinpath("../../../schemas/drawings.yml"), 24 | Path("schemas/drawings.yml"), 25 | ], 26 | "Unable to find drawing schema to run tests", 27 | ) 28 | 29 | 30 | def schema_path(): 31 | return _first_path( 32 | [ 33 | Path(CURRENT_DIR).joinpath("../../../schemas/racksdb.yml"), 34 | Path("schemas/racksdb.yml"), 35 | ], 36 | "Unable to find schema to run tests", 37 | ) 38 | 39 | 40 | def db_path(): 41 | return _first_path( 42 | [Path(CURRENT_DIR).joinpath("../../../examples/db"), Path("examples/db")], 43 | "Unable to find database to run tests", 44 | ) 45 | 46 | 47 | def db_one_file_path(): 48 | return _first_path( 49 | [ 50 | Path(CURRENT_DIR).joinpath("../../../examples/simple/racksdb.yml"), 51 | Path("examples/simple/racksdb.yml"), 52 | ], 53 | "Unable to find one file database to run tests", 54 | ) 55 | 56 | 57 | def ui_path(): 58 | # This path does not contain the full UI application but enough files to 59 | # test. 60 | return _first_path( 61 | [ 62 | Path(CURRENT_DIR).joinpath("../../../frontend/public"), 63 | Path("frontend/public"), 64 | ], 65 | "Unable to find UI public directory to run tests", 66 | ) 67 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_schema_file_loader.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | import tempfile 9 | from pathlib import Path 10 | 11 | import yaml 12 | 13 | from racksdb.generic.schema import SchemaFileLoader 14 | from racksdb.generic.errors import DBSchemaError 15 | 16 | from .lib.common import VALID_SCHEMA, VALID_EXTENSIONS 17 | 18 | 19 | class TestSchemaFileLoader(unittest.TestCase): 20 | def test_ok(self): 21 | with tempfile.NamedTemporaryFile() as tmpfile: 22 | with open(tmpfile.name, "w+") as fh: 23 | fh.write(yaml.dump(VALID_SCHEMA)) 24 | loader = SchemaFileLoader(Path(tmpfile.name)) 25 | self.assertIsInstance(loader.content, dict) 26 | 27 | def test_file_not_found(self): 28 | loader = SchemaFileLoader(Path("/dev/fail")) 29 | with self.assertRaisesRegex( 30 | DBSchemaError, "Schema path /dev/fail does not exist" 31 | ): 32 | _ = loader.content 33 | 34 | def test_extensions(self): 35 | with tempfile.TemporaryDirectory() as tmpdir: 36 | schema_path = Path(tmpdir) / "schema.yaml" 37 | ext_path = Path(tmpdir) / "extensions.yaml" 38 | with open(schema_path, "w+") as fh: 39 | fh.write(yaml.dump(VALID_SCHEMA)) 40 | with open(ext_path, "w+") as fh: 41 | fh.write(yaml.dump(VALID_EXTENSIONS)) 42 | loader = SchemaFileLoader(schema_path, ext_path) 43 | content = loader.content 44 | self.assertIsInstance(content, dict) 45 | self.assertIn("plums", content["_content"]["properties"].keys()) 46 | self.assertIn("juiciness", content["_objects"]["Pear"]["properties"].keys()) 47 | self.assertIn("Plum", content["_objects"].keys()) 48 | -------------------------------------------------------------------------------- /frontend/tests/components/ComboBox.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | import { getRouter } from 'vue-router-mock' 13 | 14 | import ComboBox from '@/components/ComboBox.vue' 15 | 16 | describe('ComboBox', () => { 17 | test('renders with datacenter items and filters correctly', async () => { 18 | const items = [ 19 | { name: 'paris', rooms: [] }, 20 | { name: 'london', rooms: [] } 21 | ] 22 | 23 | const wrapper = mount(ComboBox, { 24 | props: { 25 | itemType: 'datacenter', 26 | items 27 | } 28 | }) 29 | 30 | expect(wrapper.text()).toContain('Select a datacenter') 31 | }) 32 | 33 | test('renders with infrastructure items', () => { 34 | const items = [ 35 | { name: 'core', description: 'Core infrastructure' }, 36 | { name: 'edge', description: 'Edge infrastructure' } 37 | ] 38 | 39 | const wrapper = mount(ComboBox, { 40 | props: { 41 | itemType: 'infrastructure', 42 | items 43 | } 44 | }) 45 | 46 | expect(wrapper.text()).toContain('Select an infrastructure') 47 | }) 48 | 49 | test('navigates to correct route when item is selected', async () => { 50 | const router = getRouter() 51 | const items = [{ name: 'paris', rooms: [] }] 52 | 53 | const wrapper = mount(ComboBox, { 54 | props: { 55 | itemType: 'datacenter', 56 | items 57 | } 58 | }) 59 | 60 | // Simulate item selection 61 | wrapper.vm.goToItem('paris') 62 | await nextTick() 63 | 64 | expect(router.push).toHaveBeenCalledWith({ 65 | name: 'datacenterdetails', 66 | params: { name: 'paris' } 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /examples/db/datacenters/london.yml: -------------------------------------------------------------------------------- 1 | tags: 2 | - freecooling 3 | - tier3 4 | rooms: 5 | - name: level1 6 | dimensions: 7 | width: 15m 8 | depth: 8m 9 | rows: 10 | - name: A 11 | reversed: true 12 | position: 13 | width: 3m 14 | depth: 2m 15 | rotation: 90 16 | racks: 17 | - name: rackA[01-06] 18 | type: standard 19 | - name: B 20 | position: 21 | width: 6m 22 | depth: 2m 23 | rotation: 90 24 | racks: 25 | - name: rackB[01-06] 26 | type: standard 27 | - name: C 28 | reversed: true 29 | position: 30 | width: 10m 31 | depth: 2m 32 | rotation: 90 33 | racks: 34 | - name: rackC[01-06] 35 | type: standard 36 | - name: D 37 | position: 38 | width: 13m 39 | depth: 2m 40 | rotation: 90 41 | racks: 42 | - name: rackD[01-06] 43 | type: standard 44 | - name: level2 45 | dimensions: 46 | width: 15m 47 | depth: 8m 48 | rows: 49 | - name: E 50 | reversed: true 51 | position: 52 | width: 3m 53 | depth: 2m 54 | rotation: 90 55 | racks: 56 | - name: rackE[01-06] 57 | type: standard 58 | - name: F 59 | position: 60 | width: 6m 61 | depth: 2m 62 | rotation: 90 63 | racks: 64 | - name: rackF[01-06] 65 | type: standard 66 | - name: G 67 | reversed: true 68 | position: 69 | width: 10m 70 | depth: 2m 71 | rotation: 90 72 | racks: 73 | - name: rackG[01-06] 74 | type: standard 75 | - name: H 76 | position: 77 | width: 13m 78 | depth: 2m 79 | rotation: 90 80 | racks: 81 | - name: rackH[01-06] 82 | type: standard 83 | location: 84 | longitude: -0.075278 85 | latitude: 51.505554 86 | -------------------------------------------------------------------------------- /frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | /*Copyright (c) 2022-2023 Rackslab 2 | 3 | This file is part of RacksDB. 4 | 5 | SPDX-License-Identifier: MIT */ 6 | 7 | import { createRouter, createWebHistory } from 'vue-router' 8 | import HomeView from '../views/HomeView.vue' 9 | import DatacentersView from '../views/DatacentersView.vue' 10 | import DatacenterDetailsView from '../views/DatacenterDetailsView.vue' 11 | import DatacenterRoomView from '../views/DatacenterRoomView.vue' 12 | import InfrastructuresView from '../views/InfrastructuresView.vue' 13 | import InfrastructureDetailsView from '../views/InfrastructureDetailsView.vue' 14 | 15 | const router = createRouter({ 16 | history: createWebHistory(import.meta.env.BASE_URL), 17 | routes: [ 18 | { 19 | path: '/', 20 | name: 'home', 21 | component: HomeView, 22 | meta: { entry: 'home' } 23 | }, 24 | 25 | { 26 | path: '/datacenters', 27 | name: 'datacenters', 28 | component: DatacentersView, 29 | meta: { entry: 'datacenters' } 30 | }, 31 | 32 | { 33 | path: '/datacenters/:name', 34 | name: 'datacenterdetails', 35 | component: DatacenterDetailsView, 36 | props: true, 37 | meta: { entry: 'datacenters' } 38 | }, 39 | 40 | { 41 | path: '/datacenters/:datacenterName/:datacenterRoom', 42 | name: 'datacenterroom', 43 | component: DatacenterRoomView, 44 | props: true, 45 | meta: { entry: 'datacenters' } 46 | }, 47 | 48 | { 49 | path: '/infrastructures', 50 | name: 'infrastructures', 51 | component: InfrastructuresView, 52 | meta: { entry: 'infrastructures' } 53 | }, 54 | 55 | { 56 | path: '/infrastructures/:name', 57 | name: 'infrastructuredetails', 58 | component: InfrastructureDetailsView, 59 | props: true, 60 | meta: { entry: 'infrastructures' } 61 | } 62 | ] 63 | }) 64 | 65 | export default router 66 | -------------------------------------------------------------------------------- /racksdb/tests/generic/dumpers/test_dumper_json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | import json 9 | 10 | from racksdb.generic.dumpers.json import DBDumperJSON 11 | 12 | from ..lib.common import valid_db 13 | 14 | 15 | class TestDBDumperJSON(unittest.TestCase): 16 | def test_dump_list(self): 17 | db = valid_db() 18 | dumper = DBDumperJSON() 19 | result = dumper.dump(db.apples) 20 | json.loads(result) 21 | 22 | def test_dump_folded(self): 23 | db = valid_db() 24 | dumper = DBDumperJSON() 25 | result = dumper.dump(db.stock) 26 | stock = json.loads(result) 27 | self.assertEqual(len(stock["content"]), 2) 28 | 29 | def test_dump_expanded(self): 30 | db = valid_db() 31 | dumper = DBDumperJSON(fold=False) 32 | result = dumper.dump(db.stock) 33 | stock = json.loads(result) 34 | self.assertEqual(len(stock["content"]), 20) 35 | 36 | def test_dump_recursion(self): 37 | db = valid_db() 38 | dumper = DBDumperJSON() 39 | # Check recursion error is raised 40 | with self.assertRaisesRegex(ValueError, "Circular reference detected"): 41 | dumper.dump(db.bananas) 42 | 43 | def test_dump_map_attribute(self): 44 | db = valid_db() 45 | dumper = DBDumperJSON(objects_map={"TestBananaOrigin": "origin"}) 46 | result = dumper.dump(db.bananas) 47 | bananas = json.loads(result) 48 | self.assertEqual(bananas[0]["species"][0]["origin"], bananas[0]["origin"]) 49 | 50 | def test_dump_map_none(self): 51 | db = valid_db() 52 | dumper = DBDumperJSON(objects_map={"TestBananaOrigin": None}) 53 | result = dumper.dump(db.bananas) 54 | bananas = json.loads(result) 55 | self.assertNotIn("origin", bananas[0]["species"][0]) 56 | -------------------------------------------------------------------------------- /frontend/tests/views/DatacenterDetailsView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | import DatacenterDetailsView from '@/views/DatacenterDetailsView.vue' 14 | 15 | const datacentersMock = vi.fn() 16 | 17 | vi.mock('@/plugins/http', () => ({ 18 | useHttp: () => ({}) 19 | })) 20 | 21 | vi.mock('@/composables/RacksDBAPI', () => ({ 22 | useRacksDBAPI: () => ({ 23 | datacenters: datacentersMock 24 | }) 25 | })) 26 | 27 | describe('DatacenterDetailsView', () => { 28 | beforeEach(() => { 29 | vi.clearAllMocks() 30 | }) 31 | 32 | const flushPromises = () => new Promise((r) => setTimeout(r)) 33 | 34 | test('loads datacenter details and filters rooms by input', async () => { 35 | datacentersMock.mockResolvedValueOnce([ 36 | { 37 | name: 'paris', 38 | rooms: [ 39 | { name: 'alpha', dimensions: { width: 1000, depth: 2000 }, rows: [{ nbracks: 2 }] }, 40 | { name: 'beta', dimensions: { width: 1000, depth: 1000 }, rows: [{ nbracks: 1 }] } 41 | ] 42 | } 43 | ]) 44 | 45 | const wrapper = mount(DatacenterDetailsView, { 46 | props: { name: 'paris' }, 47 | global: { 48 | stubs: { 49 | BreadCrumbs: true 50 | } 51 | } 52 | }) 53 | 54 | await flushPromises() 55 | await nextTick() 56 | 57 | // Ensure two rooms initially 58 | let rows = wrapper.findAll('tbody tr') 59 | expect(rows.length).toBeGreaterThan(0) 60 | 61 | // Filter 62 | const input = wrapper.find('input[type="text"]') 63 | await input.setValue('alpha') 64 | await nextTick() 65 | 66 | rows = wrapper.findAll('tbody tr') 67 | expect(rows.length).toBeGreaterThan(0) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /frontend/tests/components/BreadCrumbs.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | 12 | import { getRouter } from 'vue-router-mock' 13 | 14 | import BreadCrumbs from '@/components/BreadCrumbs.vue' 15 | 16 | describe('BreadCrumbs', () => { 17 | test('renders logo and datacenter breadcrumb when route meta entry is datacenters', () => { 18 | const router = getRouter() 19 | router.currentRoute.value = { 20 | meta: { entry: 'datacenters' }, 21 | name: 'datacenterdetails' 22 | } 23 | 24 | const wrapper = mount(BreadCrumbs, { 25 | props: { 26 | datacenterName: 'paris' 27 | } 28 | }) 29 | 30 | expect(wrapper.find('img[alt="Rackslab logo"]').exists()).toBe(true) 31 | expect(wrapper.text()).toContain('Datacenters') 32 | expect(wrapper.text()).toContain('paris') 33 | }) 34 | 35 | test('renders infrastructure breadcrumb when route meta entry is infrastructures', () => { 36 | const router = getRouter() 37 | router.currentRoute.value = { 38 | meta: { entry: 'infrastructures' }, 39 | name: 'infrastructuredetails' 40 | } 41 | 42 | const wrapper = mount(BreadCrumbs, { 43 | props: { 44 | infrastructureName: 'core' 45 | } 46 | }) 47 | 48 | expect(wrapper.text()).toContain('Infrastructures') 49 | expect(wrapper.text()).toContain('core') 50 | }) 51 | 52 | test('renders room breadcrumb when route name is datacenterroom', () => { 53 | const router = getRouter() 54 | router.currentRoute.value = { 55 | meta: { entry: 'datacenters' }, 56 | name: 'datacenterroom' 57 | } 58 | 59 | const wrapper = mount(BreadCrumbs, { 60 | props: { 61 | datacenterName: 'paris', 62 | datacenterRoom: 'room1' 63 | } 64 | }) 65 | 66 | expect(wrapper.text()).toContain('paris') 67 | expect(wrapper.text()).toContain('room1') 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /docs/modules/db/partials/deftypes.adoc: -------------------------------------------------------------------------------- 1 | [#deftype] 2 | == Defined Types 3 | 4 | :url-deftypes: https://github.com/rackslab/racksdb/blob/main/racksdb/dtypes 5 | 6 | [cols="2a,2a,1l,3a"] 7 | |=== 8 | |Name|Examples|Resulting type|Comment 9 | 10 | |[#deftype-angle]`~angle` 11 | {url-deftypes}/angle.py[icon:code[]] 12 | |* `90` 13 | * `120` 14 | |int 15 | |The resulting value is angle in *degrees*. RacksDB does not accept values below 16 | 0 or above 360. 17 | 18 | |[#deftype-bits]`~bits` 19 | {url-deftypes}/bits.py[icon:code[]] 20 | |* `1.2Gb` 21 | * `3Mb` 22 | |int 23 | |The resulting value is the *number of bits*. 24 | 25 | |[#deftype-bytes]`~bytes` 26 | {url-deftypes}/bytes.py[icon:code[]] 27 | |* `32GB` 28 | * `8TB` 29 | |int 30 | |The resulting value is the *number of bytes*. 31 | 32 | |[#deftype-dimension]`~dimension` 33 | {url-deftypes}/dimension.py[icon:code[]] 34 | |* `23cm` 35 | * `1.56m` 36 | * `1900mm` 37 | |int 38 | |The resulting value is the *number of millimeters*. 39 | 40 | |[#deftype-netif_type]`~netif_type` 41 | {url-deftypes}/netif_type.py[icon:code[]] 42 | |* `ethernet` 43 | * `infiniband` 44 | |str 45 | |The resulting value is the *name of network interface type*. 46 | 47 | |[#deftype-rack_height]`~rack_height` 48 | {url-deftypes}/rack_height.py[icon:code[]] 49 | |* `1u` 50 | * `2u` 51 | |int 52 | |The resulting value is the *number of rack units* (aka. U). 53 | 54 | |[#deftype-rack_width]`~rack_width` 55 | {url-deftypes}/rack_width.py[icon:code[]] 56 | |* `full` 57 | * `1/2` 58 | * `1/8` 59 | * `1` 60 | |float 61 | |The resulting value is *the normalized part of the rack width* (_ex:_ 1.0 is 62 | 100% of the rack width, 0.5 is 50% of the rack width). The special value `full` 63 | is equals to 1. 64 | 65 | |[#deftype-storage_type]`~storage_type` 66 | {url-deftypes}/storage_type.py[icon:code[]] 67 | |* `disk` 68 | * `ssd` 69 | * `nvme` 70 | |str 71 | |The resulting value is the *name of storage device type*. 72 | 73 | |[#deftype-watts]`~watts` 74 | {url-deftypes}/watts.py[icon:code[]] 75 | |* `800W` 76 | * `1.5KW` 77 | |int 78 | |The resulting value is the *number of watts*. 79 | |=== 80 | -------------------------------------------------------------------------------- /docs/modules/db/partials/prop-types.adoc: -------------------------------------------------------------------------------- 1 | [#object] 2 | == Objects Definitions 3 | 4 | Objects definitions are mappings with the following expected keys: 5 | 6 | `description` _(optional)_:: String to describe the purpose of the object. 7 | 8 | `properties`:: Mapping of objects properties. The keys are the names of the 9 | properties. The values are mapping with the following expected keys: 10 | 11 | `type`::: The xref:#type[property type], as described below. 12 | 13 | `description`::: String to describe the purpose and content of the property. 14 | 15 | `key` _(optional)_::: Boolean to determine if the property is a 16 | xref:concepts.adoc#key[key]. Default value is _false_. 17 | 18 | `default` _(optional)_::: Default value of the property. When the `type` is an 19 | xref:concepts.adoc#object[object], the special value `:recursive` can be used to 20 | make the default value of the property the corresponding object with all its 21 | defaults properties, recursively. 22 | 23 | `example` _(optional)_::: Example value of the property. 24 | 25 | `optional` _(optional)_::: Boolean to determine if property is optional. 26 | Default value is _false_. 27 | 28 | `computed` _(optional)_::: Boolean to determine if property is 29 | xref:concepts.adoc#computed[automatically computed]. Default value is _false_. 30 | 31 | [#type] 32 | == Properties Type 33 | 34 | The xref:concepts.adoc#types[properties types] are expressed with the following 35 | notation: 36 | 37 | * xref:concepts.adoc#native[native types] are expressed by their name + 38 | _ex:_ _str_ or _int_ 39 | * xref:concepts.adoc#object [objects] are prefixed by `:` + 40 | _ex:_ `:DatacenterRoomDimensions` 41 | * xref:concepts.adoc#sequence[sequences] (or lists) are noted `list[]` + 42 | _ex:_ `list[str]` or `list[:Infrastructure]` 43 | * xref:concepts.adoc#reference[reference to other objects properties] are 44 | prefixed by `$` + 45 | _ex:_ `$NodeType.id` 46 | * xref:concepts.adoc#backref[back references] to a parent objects or one of 47 | their properties are prefixed by `^` + 48 | _ex:_ `^Datacenter` or `^InfrastructurePart.rack` 49 | * xref:structure.adoc#deftypes[defined types] are prefixed by `~` + 50 | _ex:_ `~dimension` 51 | -------------------------------------------------------------------------------- /docs/utils/schema-objs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (C) 2022 Rackslab 4 | # 5 | # This file is part of RacksDB. 6 | # 7 | # RacksDB is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # RacksDB is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with RacksDB. If not, see . 19 | 20 | """ 21 | To generate partial of database objects reference documentation, run this 22 | command: 23 | 24 | $ python3 docs/utils/schema-objs.py > docs/modules/db/partials/objects.adoc 25 | """ 26 | 27 | from pathlib import Path 28 | 29 | import jinja2 30 | 31 | from racksdb import RacksDB 32 | from racksdb.generic.schema import SchemaBackReference 33 | 34 | 35 | def bases(obj): 36 | """Jinja2 Filter to list of parent classes names of an object.""" 37 | return [_class.__name__ for _class in obj.__class__.__bases__] 38 | 39 | 40 | def main(): 41 | # Load the schema, the DB does not matter here. 42 | db = RacksDB.load( 43 | schema=Path("../schemas/racksdb.yml"), 44 | db=Path("../examples/simple/racksdb.yml"), 45 | ) 46 | 47 | # Enrich schema object with has_backref boolean 48 | for obj in db._schema.objects.values(): 49 | obj.has_backref = False 50 | for prop in obj.properties: 51 | if isinstance(prop.type, SchemaBackReference): 52 | obj.has_backref = True 53 | 54 | # Render template 55 | env = jinja2.Environment(loader=jinja2.FileSystemLoader("utils")) 56 | env.filters["bases"] = bases 57 | template = env.get_template("schema-objs.adoc.j2") 58 | output = template.render( 59 | schema=db._schema, 60 | object_prefix="RacksDB", 61 | deftype_prefix="racksdb.dtypes.", 62 | ) 63 | print(output) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | -------------------------------------------------------------------------------- /racksdb/tests/drawers/test_parameters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | import yaml 9 | from pathlib import Path 10 | import tempfile 11 | 12 | from racksdb.generic.db import DBDictsLoader, DBSplittedFilesLoader 13 | from racksdb.drawers.parameters import DrawingParameters 14 | from racksdb.generic.errors import DBFormatError 15 | from ..lib.common import drawing_schema_path 16 | 17 | 18 | class TestRacksDBDrawingParameters(unittest.TestCase): 19 | def test_load(self): 20 | DrawingParameters.load(DBDictsLoader(), drawing_schema_path()) 21 | 22 | def test_load_ext_yaml(self): 23 | with tempfile.TemporaryDirectory() as tmpdir: 24 | drawing_parameters_db_path = Path(tmpdir) / "params.yaml" 25 | with open(drawing_parameters_db_path, "w+") as fh: 26 | fh.write(yaml.dump({"margin": {"left": 10, "top": 10}})) 27 | DrawingParameters.load( 28 | DBSplittedFilesLoader(drawing_parameters_db_path), drawing_schema_path() 29 | ) 30 | 31 | def test_load_ext_yml(self): 32 | with tempfile.TemporaryDirectory() as tmpdir: 33 | drawing_parameters_db_path = Path(tmpdir) / "params.yml" 34 | with open(drawing_parameters_db_path, "w+") as fh: 35 | fh.write(yaml.dump({"margin": {"left": 10, "top": 10}})) 36 | DrawingParameters.load( 37 | DBSplittedFilesLoader(drawing_parameters_db_path), drawing_schema_path() 38 | ) 39 | 40 | def test_load_ext_fail(self): 41 | with tempfile.TemporaryDirectory() as tmpdir: 42 | drawing_parameters_db_path = Path(tmpdir) / "params.fail" 43 | with open(drawing_parameters_db_path, "w+") as fh: 44 | fh.write(yaml.dump({"margin": {"left": 10, "top": 10}})) 45 | # FIXME: should raise RacksDBFormatError 46 | with self.assertRaisesRegex( 47 | DBFormatError, 48 | r"^DB contains file \/.*\/params\.fail without valid extensions: .*$", 49 | ): 50 | DrawingParameters.load( 51 | DBSplittedFilesLoader(drawing_parameters_db_path), 52 | drawing_schema_path(), 53 | ) 54 | -------------------------------------------------------------------------------- /racksdb/tests/generic/dumpers/test_dumper_yaml.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | import yaml 10 | 11 | from racksdb.generic.dumpers.yaml import DBDumperYAML, SchemaDumperYAML 12 | 13 | from ..lib.common import valid_db, valid_schema 14 | 15 | 16 | class TestDBDumperYAML(unittest.TestCase): 17 | def test_dump_list(self): 18 | db = valid_db() 19 | dumper = DBDumperYAML() 20 | result = dumper.dump(db.apples) 21 | yaml.safe_load(result) 22 | 23 | def test_dump_show_types(self): 24 | db = valid_db() 25 | dumper = DBDumperYAML(show_types=True) 26 | dumper.dump(db.apples) 27 | 28 | def test_dump_folded(self): 29 | db = valid_db() 30 | dumper = DBDumperYAML() 31 | result = dumper.dump(db.stock) 32 | stock = yaml.safe_load(result) 33 | self.assertEqual(len(stock["content"]), 2) 34 | 35 | def test_dump_expanded(self): 36 | db = valid_db() 37 | dumper = DBDumperYAML(fold=False) 38 | result = dumper.dump(db.stock) 39 | stock = yaml.safe_load(result) 40 | self.assertEqual(len(stock["content"]), 20) 41 | 42 | def test_dump_recursion(self): 43 | db = valid_db() 44 | dumper = DBDumperYAML() 45 | with self.assertLogs("racksdb", "ERROR") as cm: 46 | result = dumper.dump(db.bananas) 47 | # Check one error has been logged and check its value. 48 | self.assertEqual(len(cm.output), 1) 49 | self.assertIn( 50 | "ERROR:racksdb.generic.dumpers.yaml:Recursion loop detected during dump, " 51 | "last represented objects:", 52 | cm.output[0], 53 | ) 54 | self.assertEqual(result, "") 55 | 56 | def test_dump_map_attribute(self): 57 | db = valid_db() 58 | dumper = DBDumperYAML(objects_map={"TestBananaOrigin": "origin"}) 59 | result = dumper.dump(db.bananas) 60 | bananas = yaml.safe_load(result) 61 | self.assertEqual(bananas[0]["species"][0]["origin"], bananas[0]["origin"]) 62 | 63 | def test_dump_map_none(self): 64 | db = valid_db() 65 | dumper = DBDumperYAML(objects_map={"TestBananaOrigin": None}) 66 | result = dumper.dump(db.bananas) 67 | bananas = yaml.safe_load(result) 68 | self.assertNotIn("origin", bananas[0]["species"][0]) 69 | 70 | 71 | class TestSchemaDumperYAML(unittest.TestCase): 72 | def test_dump(self): 73 | result = SchemaDumperYAML().dump(valid_schema()) 74 | yaml.safe_load(result) 75 | -------------------------------------------------------------------------------- /docs/utils/drawings-schema-objs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # 3 | # Copyright (C) 2022 Rackslab 4 | # 5 | # This file is part of RacksDB. 6 | # 7 | # RacksDB is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # RacksDB is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with RacksDB. If not, see . 19 | 20 | """ 21 | To generate partial of drawing parameters database objects reference documentation, 22 | run this command: 23 | 24 | $ python3 docs/utils/drawings-schema-objs.py > \ 25 | modules/usage/partials/drawing-objects.adoc 26 | """ 27 | 28 | from pathlib import Path 29 | 30 | import jinja2 31 | import yaml 32 | 33 | from racksdb.drawers.parameters import DrawingParameters 34 | from racksdb.generic.schema import ( 35 | Schema, 36 | SchemaFileLoader, 37 | SchemaDefinedTypeLoader, 38 | SchemaBackReference, 39 | ) 40 | 41 | 42 | def bases(obj): 43 | """Jinja2 Filter to list of parent classes names of an object.""" 44 | return [_class.__name__ for _class in obj.__class__.__bases__] 45 | 46 | 47 | def toyaml(stuff): 48 | return yaml.dump(stuff).rstrip() 49 | 50 | 51 | def main(): 52 | # Load the schema, the DB does not matter here. 53 | drawing_schema = Schema( 54 | SchemaFileLoader(Path("../schemas/drawings.yml")), 55 | SchemaDefinedTypeLoader(DrawingParameters.DEFINED_TYPES_MODULE), 56 | ) 57 | 58 | # Enrich schema object with has_backref boolean 59 | for obj in drawing_schema.objects.values(): 60 | obj.has_backref = False 61 | for prop in obj.properties: 62 | if isinstance(prop.type, SchemaBackReference): 63 | obj.has_backref = True 64 | 65 | # Render template 66 | env = jinja2.Environment(loader=jinja2.FileSystemLoader("utils")) 67 | env.filters["bases"] = bases 68 | env.filters["toyaml"] = toyaml 69 | template = env.get_template("schema-objs.adoc.j2") 70 | output = template.render( 71 | schema=drawing_schema, 72 | object_prefix="Drawings", 73 | deftype_prefix="racksdb.drawers.dtypes.", 74 | ) 75 | print(output) 76 | 77 | 78 | if __name__ == "__main__": 79 | main() 80 | -------------------------------------------------------------------------------- /docs/modules/usage/pages/racksdb-web.adoc: -------------------------------------------------------------------------------- 1 | ifeval::["{backend}" != "manpage"] 2 | = `racksdb-web` CLI manpage 3 | 4 | This section explains all `racksdb-web` command options available. The content 5 | of the page this also available in `racksdb-web(1)` manpage after installation. 6 | 7 | endif::[] 8 | 9 | :!example-caption: 10 | 11 | == Synopsis 12 | 13 | [.cli-opt]#*racksdb-web*# `[_OPTIONS_]` 14 | 15 | == Options 16 | 17 | [.cli-opt]#*-h, --help*#:: 18 | Show this help message and exit. 19 | 20 | [.cli-opt]#*-v, --version*#:: 21 | Show RacksDB version number and exit. 22 | 23 | [.cli-opt]#*--debug*#:: 24 | Enable debug mode with more messages in output. 25 | 26 | [.cli-opt]#*-b, --db*=#[.cli-optval]##_DB_##:: 27 | Path to database. Both files and directories paths are accepted. If the path 28 | is a directory, all YAML files in this directory are loaded, recursively. If 29 | the path does not exist, an error is reported. Default value is 30 | [.path]#`/var/lib/racksdb/`# directory. 31 | 32 | [.cli-opt]#*-s, --schema*=#[.cli-optval]##_SCHEMA_##:: 33 | Path to RacksDB schema YAML file. If the file does not exist, an error is 34 | reported. Default value is [.path]#`/usr/share/racksdb/schemas/racksdb.yml`#. 35 | 36 | [.cli-opt]#*-e, --ext*=#[.cli-optval]##_EXT_##:: 37 | Path to optional RacksDB schema extensions. If the file does not exist, 38 | it is silently ignored by RacksDB. Default value is 39 | [.path]#`/etc/racksdb/extensions.yml`#. 40 | 41 | [.cli-opt]#*--host*=#[.cli-optval]##_HOST_##:: 42 | The hostname to listen for incoming requests. Set to `0.0.0.0` to listen on 43 | all IP addresses of the host. Default value is `localhost` which restricts 44 | connections to local host. 45 | 46 | [.cli-opt]#*-p, --port*=#[.cli-optval]##_PORT_##:: 47 | TCP port to listen for incoming requests. Default value is 5000. 48 | 49 | [.cli-opt]#*--drawings-schema*=#[.cli-optval]##_DRAWINGS_SCHEMA_##:: 50 | Path to drawing parameters schema YAML file. Default value is 51 | [.path]#`/usr/share/racksdb/drawings.yml`#. 52 | 53 | [.cli-opt]#*--cors*#:: 54 | Enable CORS headers. 55 | 56 | [.cli-opt]#*--openapi*#:: 57 | Enable OpenAPI endpoint to retrieve REST API specifications. 58 | 59 | [.cli-opt]#*--with-ui*#[.cli-optval]##[=_PATH_]##:: 60 | Enable web UI. The web UI is disabled by default. When the option is used 61 | without value, default UI path [.path]#`/usr/share/racksdb/frontend`# is used. 62 | When a path is provided, `racksdb-web` loads UI files from this path. 63 | 64 | == Exit status 65 | 66 | *0*:: 67 | `racksdb-web` has processed command with success. 68 | 69 | *1*:: 70 | `racksdb-web` encountered an error. 71 | -------------------------------------------------------------------------------- /racksdb/tests/generic/lib/views.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import typing as t 8 | 9 | from racksdb.generic.views import ( 10 | DBViewSet, 11 | DBView, 12 | DBViewParameter, 13 | DBViewFilter, 14 | DBAction, 15 | DBActionParameter, 16 | DBActionResponse, 17 | DBActionError, 18 | ) 19 | 20 | 21 | class TestDBViews(DBViewSet): 22 | VIEWS = [ 23 | DBView( 24 | content="apples", 25 | objects_name="TestDBApple", 26 | description="Get information about apples", 27 | filters=[ 28 | DBViewFilter(name="name", description="Filter apples by name"), 29 | DBViewFilter( 30 | name="color", description="Filter apples by color", nargs="*" 31 | ), 32 | ], 33 | objects_map={}, 34 | ), 35 | ] 36 | PARAMETERS = [ 37 | DBViewParameter( 38 | "format", 39 | "Select output format", 40 | choices=["yaml", "json"], 41 | ), 42 | ] 43 | ACTIONS = [ 44 | DBAction( 45 | name="sell", 46 | path="/sell", 47 | description="Sell fruits", 48 | methods=["GET", "POST"], 49 | parameters=[ 50 | DBActionParameter( 51 | "type", description="Type of fruits", default="apple" 52 | ), 53 | DBActionParameter( 54 | "negotiable", 55 | description="Can price be negociated?", 56 | nargs=0, 57 | ), 58 | DBActionParameter( 59 | "buyer", 60 | description="Name of buyer", 61 | specific="cli", 62 | ), 63 | DBActionParameter( 64 | "parameters", 65 | description="Sales parameters", 66 | schema="Test", 67 | in_body=True, 68 | ), 69 | ], 70 | responses=[ 71 | DBActionResponse("application/json", schema=int), 72 | DBActionResponse("application/x-yaml", schema=t.List[str]), 73 | ], 74 | errors=[ 75 | DBActionError( 76 | 400, 77 | "Missing request parameter.", 78 | ), 79 | DBActionError( 80 | 404, 81 | "Type of fruits not found in database.", 82 | ), 83 | ], 84 | ), 85 | ] 86 | -------------------------------------------------------------------------------- /frontend/tests/views/DatacenterRoomView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | import DatacenterRoomView from '@/views/DatacenterRoomView.vue' 14 | 15 | const datacentersMock = vi.fn() 16 | const infrastructuresMock = vi.fn() 17 | const roomImageSvgMock = vi.fn() 18 | 19 | vi.mock('@/plugins/http', () => ({ 20 | useHttp: () => ({}) 21 | })) 22 | 23 | vi.mock('@/composables/RacksDBAPI', () => ({ 24 | useRacksDBAPI: () => ({ 25 | datacenters: datacentersMock, 26 | infrastructures: infrastructuresMock, 27 | roomImageSvg: roomImageSvgMock 28 | }) 29 | })) 30 | 31 | describe('DatacenterRoomView', () => { 32 | beforeEach(() => { 33 | vi.clearAllMocks() 34 | }) 35 | 36 | const flushPromises = () => new Promise((resolve) => setTimeout(resolve)) 37 | 38 | test('lists racks for the selected room and filters with input', async () => { 39 | infrastructuresMock.mockResolvedValueOnce([{ name: 'core', layout: [] }]) 40 | 41 | datacentersMock.mockResolvedValueOnce([ 42 | { 43 | name: 'paris', 44 | rooms: [ 45 | { 46 | name: 'roomA', 47 | rows: [ 48 | { 49 | racks: [ 50 | { name: 'R01', fillrate: 0.1 }, 51 | { name: 'R02', fillrate: 0 } 52 | ] 53 | } 54 | ] 55 | } 56 | ] 57 | } 58 | ]) 59 | 60 | roomImageSvgMock.mockResolvedValueOnce(new Blob([''], { type: 'image/svg+xml' })) 61 | 62 | const wrapper = mount(DatacenterRoomView, { 63 | props: { 64 | datacenterName: 'paris', 65 | datacenterRoom: 'roomA' 66 | }, 67 | global: { 68 | stubs: { 69 | BreadCrumbs: true, 70 | DatacenterListInfrastructures: true 71 | } 72 | } 73 | }) 74 | 75 | await flushPromises() 76 | await nextTick() 77 | 78 | // Initially, both racks are present 79 | expect(wrapper.findAll('tbody tr').length).toBeGreaterThan(0) 80 | 81 | // Filter by name 82 | const input = wrapper.find('input[type="text"]') 83 | await input.setValue('R01') 84 | await nextTick() 85 | 86 | // Only one rack should remain visible in filtered table 87 | const rows = wrapper.findAll('tbody tr') 88 | // Count rows with real rack entries (skip possible empty state rows) 89 | const rackRows = rows.filter((tr) => tr.findAll('td').length === 3) 90 | expect(rackRows.length).toBe(1) 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /racksdb/tests/lib/reference.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | # Expected values from testing reference DB. 10 | REFDB_DATACENTERS = ["paris", "london"] 11 | REFDB_INFRASTRUCTURES = ["mercury", "jupiter", "sharednet"] 12 | REFDB_TOTAL_NODES = 130 13 | 14 | 15 | class TestRacksDBReferenceDB(unittest.TestCase): 16 | """Abstract TestCase class with method to validate data from testing reference 17 | database.""" 18 | 19 | def assertDatacentersResponse(self, content): 20 | self.assertIsInstance(content, list) 21 | self.assertEqual(len(content), 2) 22 | self.assertCountEqual( 23 | [datacenter["name"] for datacenter in content], REFDB_DATACENTERS 24 | ) 25 | self.assertIsInstance(content[0], dict) 26 | # tags key is optional, it may not be present in selected datacenter. 27 | for key in ["name", "rooms", "location"]: 28 | self.assertIn( 29 | key, 30 | content[0], 31 | ) 32 | 33 | def assertInfrastructuresResponse(self, content): 34 | self.assertIsInstance(content, list) 35 | self.assertEqual(len(content), 3) 36 | self.assertCountEqual( 37 | [infrastructure["name"] for infrastructure in content], 38 | REFDB_INFRASTRUCTURES, 39 | ) 40 | self.assertIsInstance(content[0], dict) 41 | # tags key is optional, it may not be present in selected infrastructure. 42 | for key in ["name", "description", "layout"]: 43 | self.assertIn( 44 | key, 45 | content[0], 46 | ) 47 | 48 | def assertNodesResponse(self, content): 49 | self.assertIsInstance(content, list) 50 | self.assertEqual(len(content), 130) 51 | self.assertIsInstance(content[0], dict) 52 | self.assertCountEqual( 53 | content[0].keys(), 54 | ["name", "infrastructure", "rack", "type", "slot", "tags", "position"], 55 | ) 56 | 57 | def assertRacksResponse(self, content): 58 | self.assertIsInstance(content, list) 59 | self.assertEqual(len(content), 101) 60 | self.assertIsInstance(content[0], dict) 61 | # tags key is optional, it may not be present in select rack. 62 | for key in [ 63 | "name", 64 | "slot", 65 | "type", 66 | "datacenter", 67 | "room", 68 | "row", 69 | "nodes", 70 | "fillrate", 71 | ]: 72 | self.assertIn( 73 | key, 74 | content[0].keys(), 75 | ) 76 | -------------------------------------------------------------------------------- /frontend/tests/components/FiltersBar.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | 12 | import FiltersBar from '@/components/FiltersBar.vue' 13 | 14 | describe('FiltersBar', () => { 15 | test('renders active filters as badges', () => { 16 | const wrapper = mount(FiltersBar, { 17 | props: { 18 | selectedRacks: ['R01', 'R02'], 19 | selectedEquipmentTypes: ['server'], 20 | selectedCategories: ['nodes'], 21 | selectedTags: ['prod'], 22 | inputEquipmentName: 'test-server' 23 | } 24 | }) 25 | 26 | expect(wrapper.text()).toContain('R01') 27 | expect(wrapper.text()).toContain('R02') 28 | expect(wrapper.text()).toContain('server') 29 | expect(wrapper.text()).toContain('nodes') 30 | expect(wrapper.text()).toContain('prod') 31 | expect(wrapper.text()).toContain('test-server') 32 | }) 33 | 34 | test('does not render empty filters', () => { 35 | const wrapper = mount(FiltersBar, { 36 | props: { 37 | selectedRacks: [], 38 | selectedEquipmentTypes: [], 39 | selectedCategories: [], 40 | selectedTags: [], 41 | inputEquipmentName: '' 42 | } 43 | }) 44 | 45 | // Should only show "Filters" label and screen reader text 46 | expect(wrapper.text()).toContain('Filters') 47 | expect(wrapper.text()).toContain('active') 48 | }) 49 | 50 | test('removes filter when X button is clicked', async () => { 51 | const wrapper = mount(FiltersBar, { 52 | props: { 53 | selectedRacks: ['R01'], 54 | selectedEquipmentTypes: [], 55 | selectedCategories: [], 56 | selectedTags: [], 57 | inputEquipmentName: '' 58 | } 59 | }) 60 | 61 | // Find and click the X button for R01 62 | const removeButton = wrapper.find('button') 63 | await removeButton.trigger('click') 64 | 65 | // Check that the filter was removed 66 | expect(wrapper.vm.selectedRacks).toEqual([]) 67 | }) 68 | 69 | test('clears equipment name filter when X button is clicked', async () => { 70 | const wrapper = mount(FiltersBar, { 71 | props: { 72 | selectedRacks: [], 73 | selectedEquipmentTypes: [], 74 | selectedCategories: [], 75 | selectedTags: [], 76 | inputEquipmentName: 'test' 77 | } 78 | }) 79 | 80 | // Find and click the X button for equipment name 81 | const removeButton = wrapper.find('button') 82 | await removeButton.trigger('click') 83 | 84 | // Check that the equipment name was cleared 85 | expect(wrapper.vm.inputEquipmentName).toBe('') 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /docs/modules/db/pages/files.adoc: -------------------------------------------------------------------------------- 1 | = Database Files 2 | 3 | == Location 4 | 5 | By default, RacksDB expects the database to be located in 6 | [.path]#`/var/lib/racksdb`# directory, splitted into multiple files. 7 | 8 | NOTE: RacksDB can load databases at the location of your choice, at the 9 | condition of specifying the path in xref:usage:racksdb.adoc[`racksdb` 10 | command] options and xref:usage:lib.adoc[library `load()`] arguments. For this 11 | reason, it is highly recommended to use the default path. 12 | 13 | [#splitted] 14 | == Files Tree 15 | 16 | RacksDB database can be stored in a single YAML file. However, for obvious 17 | maintenaibility reasons, it is recommended to split the database into multiple 18 | files. RacksDB can load a database by walking into a directory recursively. It 19 | loads all YAML formatted files with `.yml` extension. It perfoms a mapping 20 | between the files hierarchy and the resulting YAML tree. 21 | 22 | When a folder is suffixed by `.l`, RacksDB considers the files in this folder 23 | to be members of a sequence (_aka._ list or array). The content of each of these 24 | files is an item in this list. In this case, the filename is ignored. 25 | 26 | In other folders, the files are considered to be a key/value pair of a mapping 27 | (_aka_. hash or associative array). The filename without its extension is the 28 | key, the value is the content of the file. 29 | 30 | Following these principles, users are free to choose the level of fragmentation 31 | in the database model, at their own convenience. 32 | 33 | .Show basic example 34 | [%collapsible] 35 | ==== 36 | For example, consider this basic example YAML file: 37 | 38 | [source,yaml] 39 | ---- 40 | apples: 41 | - variety: granny smith 42 | color: green 43 | - variety: golden 44 | color: yellow 45 | stocks: 46 | apples: 47 | granny smith: 10 48 | golden: 20 49 | ---- 50 | 51 | It is strictly identical to this folder tree: 52 | 53 | [source] 54 | ---- 55 | 📂 apples.l/ 56 | 📄 granny.yml 57 | 📄 golden.yml 58 | 📂 stocks/ 59 | 📄 apples.yml 60 | ---- 61 | 62 | With the following files content: 63 | 64 | `granny.yml`:: 65 | 66 | [source,yaml] 67 | ---- 68 | variety: granny smith 69 | color: green 70 | ---- 71 | 72 | `golden.yml`:: 73 | 74 | [source,yaml] 75 | ---- 76 | variety: golden 77 | color: yellow 78 | ---- 79 | 80 | `apples.yml`:: 81 | 82 | [source,yaml] 83 | ---- 84 | granny smith: 10 85 | golden: 20 86 | ---- 87 | ==== 88 | 89 | == Examples 90 | 91 | When RacksDB is installed, full examples of databases are available in path 92 | [.path]#`/usr/share/doc/racksdb/examples/db/`#. Database contained in a single 93 | YAML file and databases splitted in a tree of YAML files can be found in this 94 | folder. 95 | 96 | Examples are also available in 97 | https://github.com/rackslab/racksdb/blob/main/examples/[source code Git 98 | repository]. 99 | -------------------------------------------------------------------------------- /frontend/tests/views/DatacentersView.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | import DatacentersView from '@/views/DatacentersView.vue' 14 | 15 | // Mock router used by the view 16 | vi.mock('vue-router', () => ({ 17 | useRouter: () => ({ 18 | resolve: () => ({ fullPath: '/datacenter/paris' }) 19 | }) 20 | })) 21 | 22 | // Mock HTTP plugin and API composable 23 | const datacentersMock = vi.fn() 24 | vi.mock('@/plugins/http', () => ({ 25 | useHttp: () => ({}) 26 | })) 27 | vi.mock('@/composables/RacksDBAPI', () => ({ 28 | useRacksDBAPI: () => ({ 29 | datacenters: datacentersMock 30 | }) 31 | })) 32 | 33 | // Mock Leaflet to avoid DOM-heavy behavior 34 | vi.mock('leaflet', () => { 35 | const addTo = () => ({}) 36 | const on = () => ({}) 37 | return { 38 | default: { 39 | map: () => ({ setView: () => ({ on, fitBounds: () => {} }) }), 40 | tileLayer: () => ({ addTo }), 41 | marker: () => ({ addTo: () => ({ bindPopup: () => {} }) }), 42 | latLngBounds: () => ({ extend: () => {} }), 43 | icon: () => ({}) 44 | } 45 | } 46 | }) 47 | 48 | describe('DatacentersView', () => { 49 | beforeEach(() => { 50 | vi.clearAllMocks() 51 | }) 52 | 53 | const flushPromises = () => new Promise((resolve) => setTimeout(resolve)) 54 | 55 | test('hides the map when no datacenter has a location', async () => { 56 | datacentersMock.mockResolvedValueOnce([ 57 | { name: 'paris', rooms: [], location: undefined }, 58 | { name: 'london', rooms: [], location: undefined } 59 | ]) 60 | 61 | const wrapper = mount(DatacentersView, { 62 | global: { 63 | stubs: { 64 | BreadCrumbs: true 65 | } 66 | } 67 | }) 68 | 69 | // Wait for onMounted async code 70 | await flushPromises() 71 | await nextTick() 72 | 73 | expect(wrapper.vm.showMap).toBe(false) 74 | }) 75 | 76 | test('shows the map when at least one datacenter has a location', async () => { 77 | datacentersMock.mockResolvedValueOnce([ 78 | { 79 | name: 'paris', 80 | rooms: [], 81 | location: { latitude: 48.85, longitude: 2.35 } 82 | }, 83 | { name: 'london', rooms: [], location: undefined } 84 | ]) 85 | 86 | const wrapper = mount(DatacentersView, { 87 | attachTo: document.body, 88 | global: { 89 | stubs: { 90 | BreadCrumbs: true 91 | } 92 | } 93 | }) 94 | 95 | await flushPromises() 96 | await nextTick() 97 | 98 | expect(wrapper.vm.showMap).toBe(true) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /frontend/tests/components/HomeViewCards.spec.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2025 Rackslab 3 | 4 | This file is part of RacksDB. 5 | 6 | SPDX-License-Identifier: MIT 7 | */ 8 | 9 | import { describe, test, expect, vi, beforeEach } from 'vitest' 10 | import { mount, flushPromises } from '@vue/test-utils' 11 | import { nextTick } from 'vue' 12 | 13 | // Mock API composable 14 | const datacentersMock = vi.fn() 15 | const infrastructuresMock = vi.fn() 16 | 17 | vi.mock('@/plugins/http', () => ({ 18 | useHttp: () => ({}) 19 | })) 20 | 21 | vi.mock('@/composables/RacksDBAPI', () => ({ 22 | useRacksDBAPI: () => ({ 23 | datacenters: datacentersMock, 24 | infrastructures: infrastructuresMock 25 | }) 26 | })) 27 | 28 | import HomeViewCards from '@/components/HomeViewCards.vue' 29 | 30 | describe('HomeViewCards', () => { 31 | beforeEach(() => { 32 | vi.clearAllMocks() 33 | }) 34 | 35 | test('loads and displays datacenters and infrastructures', async () => { 36 | datacentersMock.mockResolvedValueOnce([ 37 | { name: 'paris', rooms: [], tags: ['prod'] }, 38 | { name: 'london', rooms: [], tags: ['test'] } 39 | ]) 40 | 41 | infrastructuresMock.mockResolvedValueOnce([ 42 | { name: 'core', description: 'Core infrastructure', tags: ['compute'] }, 43 | { name: 'edge', description: 'Edge infrastructure', tags: ['network'] } 44 | ]) 45 | 46 | const wrapper = mount(HomeViewCards) 47 | 48 | await flushPromises() 49 | await nextTick() 50 | 51 | expect(wrapper.text()).toContain('2 datacenter') 52 | expect(wrapper.text()).toContain('2 infrastructure') 53 | expect(wrapper.text()).toContain('paris') 54 | expect(wrapper.text()).toContain('london') 55 | expect(wrapper.text()).toContain('core') 56 | expect(wrapper.text()).toContain('edge') 57 | }) 58 | 59 | test('displays singular form for single items', async () => { 60 | datacentersMock.mockResolvedValueOnce([{ name: 'paris', rooms: [], tags: [] }]) 61 | 62 | infrastructuresMock.mockResolvedValueOnce([ 63 | { name: 'core', description: 'Core infrastructure', tags: [] } 64 | ]) 65 | 66 | const wrapper = mount(HomeViewCards) 67 | 68 | await flushPromises() 69 | await nextTick() 70 | 71 | expect(wrapper.text()).toContain('1 datacenter') 72 | expect(wrapper.text()).toContain('1 infrastructure') 73 | }) 74 | 75 | test('displays tags for datacenters and infrastructures', async () => { 76 | datacentersMock.mockResolvedValueOnce([{ name: 'paris', rooms: [], tags: ['prod', 'eu'] }]) 77 | 78 | infrastructuresMock.mockResolvedValueOnce([ 79 | { name: 'core', description: 'Core infrastructure', tags: ['compute', 'storage'] } 80 | ]) 81 | 82 | const wrapper = mount(HomeViewCards) 83 | 84 | await flushPromises() 85 | await nextTick() 86 | 87 | expect(wrapper.text()).toContain('#prod') 88 | expect(wrapper.text()).toContain('#eu') 89 | expect(wrapper.text()).toContain('#compute') 90 | expect(wrapper.text()).toContain('#storage') 91 | }) 92 | }) 93 | -------------------------------------------------------------------------------- /racksdb/tests/generic/test_db_dict.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | 9 | from racksdb.generic.db import DBDict, DBObjectRange, DBExpandableObject 10 | 11 | from .lib.common import valid_db 12 | 13 | 14 | class TestDBDict(unittest.TestCase): 15 | def setUp(self): 16 | self.db = valid_db() 17 | 18 | def test_filter_ok(self): 19 | self.assertIsInstance(self.db.apples, DBDict) 20 | yellow_species = self.db.apples.filter(color="yellow") 21 | self.assertIsInstance(yellow_species, DBDict) 22 | self.assertCountEqual(yellow_species.keys(), ["golden"]) 23 | self.assertEqual(yellow_species["golden"].__class__.__name__, "TestApple") 24 | 25 | def test_filter_no_value(self): 26 | blue_species = self.db.apples.filter(color="blue") 27 | self.assertIsInstance(blue_species, DBDict) 28 | self.assertCountEqual(blue_species.keys(), []) 29 | 30 | def test_filter_invalid_criteria(self): 31 | with self.assertRaises(TypeError): 32 | self.db.apples.filter(fail="fail") 33 | 34 | def test_iter(self): 35 | nb = 0 36 | for item in self.db.apples: 37 | nb += 1 38 | self.assertEqual(nb, 2) 39 | 40 | def test_getitem(self): 41 | self.assertEqual(self.db.apples["golden"].__class__.__name__, "TestApple") 42 | 43 | def test_getitem_not_found(self): 44 | with self.assertRaisesRegex(KeyError, "fail"): 45 | self.db.apples["fail"] 46 | 47 | def test_len(self): 48 | self.assertEqual(len(self.db.apples), 2) 49 | 50 | def test_first(self): 51 | first = self.db.apples.first() 52 | self.assertEqual(first.__class__.__name__, "TestApple") 53 | 54 | 55 | class TestDBDictExpandable(unittest.TestCase): 56 | def setUp(self): 57 | self.db = valid_db() 58 | self.expandable_dict = DBDict() 59 | # Create DBDict of DBExpandableObjects with DBList of AppleCrate. 60 | for crates in self.db.stock.content.itervalues(): 61 | self.expandable_dict[crates.name] = crates 62 | self.assertIsInstance(crates.name, DBObjectRange) 63 | self.assertIsInstance(crates, DBExpandableObject) 64 | 65 | def test_expandable_filter(self): 66 | selected = self.expandable_dict.filter(quantity_min=15) 67 | self.assertIsInstance(selected, DBDict) 68 | self.assertEqual(len(selected), 10) 69 | selected = self.expandable_dict.filter(quantity_min=5) 70 | self.assertEqual(len(selected), 20) 71 | 72 | def test_expandable_iter(self): 73 | nb = 0 74 | for item in self.expandable_dict: 75 | nb += 1 76 | self.assertEqual(nb, 20) 77 | 78 | def test_expandable_getitem(self): 79 | self.assertEqual(self.expandable_dict["crate02"].name, "crate02") 80 | 81 | def test_expandable_len(self): 82 | self.assertEqual(len(self.expandable_dict), 20) 83 | 84 | def test_expandable_first(self): 85 | self.assertEqual(self.expandable_dict.first().name, "crate01") 86 | -------------------------------------------------------------------------------- /docs/modules/usage/pages/ui.adoc: -------------------------------------------------------------------------------- 1 | = Web UI 2 | 3 | This section describes all features available in Web UI pages. 4 | 5 | == Home 6 | 7 | image::shadowed/home.webp[UI Homeview,800,xref=image$raw/home.png] 8 | 9 | The *Home* page provides an overview of what's inside RacksDB in card form. 10 | Users can navigate to different sections, such as _datacenters_, _datacenter 11 | details_, _datacenter room_, _infrastructures_, _infrastructure details,_ 12 | either through the navigation bar or by clicking on the items in the cards. 13 | 14 | == Datacenters 15 | 16 | image::shadowed/datacenters.webp[UI Datacenters,800,xref=image$raw/datacenters.png] 17 | 18 | In *Datacenters* page, users can select a datacenter in the list. The 19 | datacenters can be filtered by typing portion of their name in the *search bar*. 20 | The datacenters can also be selected in *the map* by clicking on the markers. 21 | 22 | == Datacenter Details 23 | 24 | image::shadowed/rooms.webp[UI Datacenter Details,800,xref=image$raw/rooms.png] 25 | 26 | The *Datacenter Details* page displays details about the selected datacenter. 27 | Users can filter the list of rooms by typing portion of their names in the text 28 | input field in top right corner. The order of rooms can be inverted with *sort 29 | rooms* button. Users can *click on a room* to access the room details page. 30 | 31 | == Room Details 32 | 33 | image::shadowed/racks.webp[UI Datacenter Room,800,xref=image$raw/racks.png] 34 | 35 | In *Room Details* page, a thumbnail of the room map is displayed at the top. 36 | Users can *click on this thumbnail* to display a larger image. 37 | 38 | Below this image, a table contains the list of racks in the room with their 39 | _names_, _fill rates_ and _infrastructures_ using them. Infrastructures can be 40 | clicked to access infrastructure details. 41 | 42 | Racks can be filtered using the *filter bar*. The list can be inverted with 43 | *sort racks* button. Empty racks can be also hidden using the *toggle button*. 44 | 45 | == Infrastructures 46 | 47 | image::shadowed/infrastructures.webp[UI Infrastructures,800,xref=image$raw/infrastructures.png] 48 | 49 | In *Infrastructures* page, users can select an infrastructure in the list. The 50 | list can be filtered by typing portion of their name in the *search bar*. 51 | 52 | == Infrastructure Details 53 | 54 | image::shadowed/infrastructure.webp[UI Infrastructure Details,800,xref=image$raw/infrastructure.png] 55 | 56 | In *Infrastructure Details* page, a graphical representation of the 57 | infrastructure is displayed at the top. Users can *click on this thumbnail* to 58 | display a larger image. 59 | 60 | The page contains a table of all infrastructure equipment grouped by rack, 61 | including their _name_, _category_, _model_ and _tags_. 62 | 63 | Users can *show or hide racks* by clicking on racks name. Users can also click 64 | on equipment model name to *open a modal* with more details about this model. 65 | 66 | Filters are available to help users find specific equipment in infrastructures. 67 | Lists allow _selection of racks_ and _equipment types_, *checkboxes* for 68 | _equipment categories_ and _tags_ and a *search bar* to find specific equipment 69 | by name: 70 | 71 | image::shadowed/infrastructure_filters.webp[UI Infrastructure Filters,800,xref=image$raw/infrastructure_filters.png] 72 | -------------------------------------------------------------------------------- /frontend/src/components/BreadCrumbs.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 19 | 20 | 92 | -------------------------------------------------------------------------------- /frontend/src/views/InfrastructureDetailsView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 58 | 59 | 105 | -------------------------------------------------------------------------------- /racksdb/tests/drawers/test_axonometric_infrastructure.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2025 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | import unittest 8 | import tempfile 9 | from pathlib import Path 10 | 11 | from racksdb import RacksDB 12 | from racksdb.drawers.axonometric_infrastructure import AxonometricInfrastructureDrawer 13 | from racksdb.drawers.parameters import DrawingParameters 14 | from racksdb.generic.db import DBDictsLoader 15 | from racksdb.errors import RacksDBDrawingError 16 | 17 | from ..lib.common import drawing_schema_path, schema_path, db_path 18 | 19 | 20 | class TestAxonometricInfrastructureDrawer(unittest.TestCase): 21 | def setUp(self): 22 | try: 23 | self.drawings_schema_path = drawing_schema_path() 24 | self.schema_path = schema_path() 25 | self.db_path = db_path() 26 | except FileNotFoundError as err: 27 | self.skipTest(err) 28 | self.parameters = DrawingParameters.load( 29 | DBDictsLoader({}), self.drawings_schema_path 30 | ) 31 | self.db = RacksDB.load(schema=self.schema_path, db=self.db_path) 32 | 33 | def test_draw_png(self): 34 | with tempfile.TemporaryDirectory() as tmpdir: 35 | filename = Path(tmpdir) / "output.png" 36 | drawer = AxonometricInfrastructureDrawer( 37 | self.db, "mercury", filename, "png", self.parameters, None, "yaml" 38 | ) 39 | drawer.draw() 40 | self.assertTrue(filename.exists()) 41 | 42 | def test_draw_svg(self): 43 | with tempfile.TemporaryDirectory() as tmpdir: 44 | filename = Path(tmpdir) / "output.svg" 45 | drawer = AxonometricInfrastructureDrawer( 46 | self.db, "mercury", filename, "svg", self.parameters, None, "yaml" 47 | ) 48 | drawer.draw() 49 | self.assertTrue(filename.exists()) 50 | 51 | def test_draw_pdf(self): 52 | with tempfile.TemporaryDirectory() as tmpdir: 53 | filename = Path(tmpdir) / "output.pdf" 54 | drawer = AxonometricInfrastructureDrawer( 55 | self.db, "mercury", filename, "pdf", self.parameters, None, "yaml" 56 | ) 57 | drawer.draw() 58 | self.assertTrue(filename.exists()) 59 | 60 | def test_draw_not_existing_infrastructure(self): 61 | with tempfile.TemporaryDirectory() as tmpdir: 62 | filename = Path(tmpdir) / "output.png" 63 | with self.assertRaisesRegex( 64 | RacksDBDrawingError, "^Unable to find infrastructure fail in database$" 65 | ): 66 | AxonometricInfrastructureDrawer( 67 | self.db, "fail", filename, "png", self.parameters, None, "yaml" 68 | ) 69 | self.assertFalse(filename.exists()) 70 | 71 | def test_draw_no_rack_error(self): 72 | self.parameters.infrastructure.equipment_tags = "fail" 73 | with tempfile.TemporaryDirectory() as tmpdir: 74 | filename = Path(tmpdir) / "output.png" 75 | drawer = AxonometricInfrastructureDrawer( 76 | self.db, "mercury", filename, "png", self.parameters, None, "yaml" 77 | ) 78 | with self.assertRaisesRegex( 79 | RacksDBDrawingError, 80 | "^Unable to find racks to draw with filters provided in drawing " 81 | "parameters$", 82 | ): 83 | drawer.draw() 84 | self.assertFalse(filename.exists()) 85 | -------------------------------------------------------------------------------- /docs/modules/usage/pages/integrations.adoc: -------------------------------------------------------------------------------- 1 | = Integrations 2 | 3 | == Clustershell 4 | 5 | https://clustershell.readthedocs.io/en/latest/index.html[ClusterShell] is a 6 | scalable cluster administration Python framework, designed to execute commands 7 | on many cluster nodes in parallel. ClusterShell supports the possibility to 8 | define groups of nodes. These groups can be extracted from external sources. 9 | 10 | This section illustrates how to make ClusterShell extracts groups from RacksDB 11 | and tags associated to nodes with a simple example. In this example, let's 12 | consider a cluster infrastructure named _atlas_. 13 | 14 | Create file [.path]#`/etc/clustershell/groups.conf.d/racksdb.conf`# with this content: 15 | 16 | [source,ini] 17 | ---- 18 | [racksdb] 19 | map: racksdb nodes --infrastructure atlas --tags $GROUP --list 20 | all: racksdb nodes --infrastructure atlas --list 21 | list: racksdb tags --infrastructure atlas --on-nodes 22 | reverse: racksdb tags --node $NODE 23 | ---- 24 | 25 | With this simple configuration, ClusterShell can extract groups and nodes from 26 | RacksDB. For example, with `nodeset` command: 27 | 28 | * Get the list of groups: 29 | + 30 | -- 31 | [source,console] 32 | ---- 33 | $ nodeset -s racksdb -l 34 | @racksdb:admin 35 | @racksdb:compute 36 | @racksdb:login 37 | ---- 38 | -- 39 | 40 | * Get all nodes in _compute_ group (ie. associated to _compute_ tag in 41 | database): 42 | + 43 | -- 44 | [source,console] 45 | ---- 46 | $ nodeset -f @racksdb:compute 47 | atcn[1-2] 48 | ---- 49 | -- 50 | 51 | * Get all nodes in this cluster: 52 | + 53 | -- 54 | [source,console] 55 | ---- 56 | $ nodeset -f -s racksdb -a 57 | atadmin,atcn[1-2],atlogin 58 | ---- 59 | -- 60 | 61 | ClusterShell can execute commands on nodes from these groups with `clush`: 62 | 63 | [source,console] 64 | ---- 65 | # clush -bw @racksdb:compute uname 66 | --------------- 67 | atcn[1-2] (2) 68 | --------------- 69 | Linux 70 | ---- 71 | 72 | This way, ClusterShell groups are always synchronized with tags and content of 73 | RacksDB database. when RacksDB database is updated, changes are automatically 74 | reflected in ClusterShell. 75 | 76 | .More than one infrastructure? 77 | **** 78 | If you need to extract groups from multiple infrastructures in RacksDB, a simple 79 | approach if to use ClusterShell 80 | https://clustershell.readthedocs.io/en/latest/config.html#multiple-sources-section[multiple sources sections] 81 | feature and `$GROUP` variable. For example, in 82 | [.path]#`/etc/clustershell/groups.conf.d/racksdb.conf`#: 83 | 84 | [source,ini] 85 | ---- 86 | [trinity,atlas] 87 | map: racksdb nodes --infrastructure $SOURCE --tags $GROUP --list 88 | all: racksdb nodes --infrastructure $SOURCE --list 89 | list: racksdb tags --infrastructure $SOURCE --on-nodes 90 | reverse: racksdb tags --node $NODE 91 | ---- 92 | 93 | This way, RacksDB commands are executed for multiple infrastructures: 94 | 95 | [source,console] 96 | ---- 97 | $ nodeset -f @trinity:compute 98 | tricn[1-4] 99 | $ nodeset -f @atlas:compute 100 | atcn[1-2] 101 | ---- 102 | 103 | Another yet more generic approach to split the group source with shell: 104 | 105 | [source,ini] 106 | ---- 107 | [racksdb] 108 | map: GRP=$GROUP; racksdb nodes --infrastructure ${GRP%:*} --tags ${GRP#*:} --list 109 | all: racksdb nodes --list 110 | reverse: racksdb tags --node $NODE 111 | ---- 112 | 113 | This way, infrastructures names are not mentioned in ClusterShell configuration 114 | file and all infrastructures defined in RacksDB can be requested: 115 | 116 | [source,console] 117 | ---- 118 | $ nodeset -f @racksdb:trinity:compute 119 | tricn[1-4] 120 | $ nodeset -f @racksdb:atlas:compute 121 | atcn[1-2] 122 | ---- 123 | **** 124 | -------------------------------------------------------------------------------- /docs/modules/usage/pages/drawparams.adoc: -------------------------------------------------------------------------------- 1 | = Drawing Parameters 2 | 3 | RacksDB offers the possibity to generate graphical representations of datacenter 4 | room and infrastructures with their racks and equipments in various images 5 | formats. The images are generated with various default drawing parameters that 6 | user are able to customize for their requirements. 7 | 8 | The xref:#usage[first section] of this page describes how to specify drawing 9 | parameters with both RacksDB xref:racksdb.adoc[CLI] and 10 | xref:rest.adoc[REST API]. The xref:#reference[second section] is the reference 11 | documentation of all available drawing parameters with their format and default 12 | values. 13 | 14 | [#usage] 15 | == Usage 16 | 17 | === CLI 18 | 19 | The `draw` subcommand of xref:racksdb.adoc[`racksdb` CLI] accepts drawing 20 | parameters with `--parameters` option. The value of this option must specify a 21 | path to a YAML file that contains the drawing parameters. 22 | 23 | .Show example `custom.yml` drawing parameters YAML file 24 | [%collapsible] 25 | ==== 26 | [source,yaml] 27 | ---- 28 | include::example$custom.yml[] 29 | ---- 30 | ==== 31 | 32 | For example, to generate graphical representation of _tiger_ infrastructure with 33 | custom drawing parameters in `custom.yml` file, run this command: 34 | 35 | [source,console] 36 | ---- 37 | $ racksdb draw infrastructure --name=tiger --parameters=custom.yml 38 | ---- 39 | 40 | Alternatively, the option accepts the special value `-`. In this case, the 41 | drawing parameters are read on command standard input (_stdin_). For example: 42 | 43 | [source,console] 44 | ---- 45 | $ racksdb draw room --name=noisy --parameters=- < custom.yaml 46 | ---- 47 | 48 | === REST API 49 | 50 | It is also possible to provide custom drawing parameters to RacksDB 51 | xref:rest.adoc[REST API]. The REST API accepts drawing parameters in both JSON 52 | and YAML formats. The parameters must be sent in requests body content with 53 | content type header set according to the format. 54 | 55 | For example, to generate graphical representation of _mercury_ infrastructure 56 | with inline drawing parameters in JSON format, the following `curl` command can 57 | be used: 58 | 59 | [source,console,subs=attributes] 60 | ---- 61 | $ curl -X POST -H 'Content-Type: application/json' \ 62 | --data '{"margin":{ "top": 200 }}' \ 63 | http://localhost:5000/{api-version}/draw/infrastructure/mercury.png \ 64 | --output mercury.png 65 | ---- 66 | 67 | Alternatively, the drawing parameters can be extracted a JSON file. For example 68 | with file `custom.json`: 69 | 70 | [source,console,subs=attributes] 71 | ---- 72 | $ curl -X POST -H 'Content-Type: application/json' \ 73 | --data @custom.json 74 | http://localhost:5000/{api-version}/draw/infrastructure/mercury.png \ 75 | --output mercury.png 76 | ---- 77 | 78 | .Show example `custom.json` drawing parameters YAML file 79 | [%collapsible] 80 | ==== 81 | [source,json] 82 | ---- 83 | include::example$custom.json[] 84 | ---- 85 | ==== 86 | 87 | To send drawing parameters in YAML format, the content type HTTP header must be 88 | changed: 89 | 90 | [source,console,subs=attributes] 91 | ---- 92 | $ curl -X POST -H 'Content-Type: application/x-yaml' \ 93 | --data-binary @custom.yml \ 94 | http://localhost:5000/{api-version}/draw/infrastructure/mercury.png \ 95 | --output mercury.png 96 | ---- 97 | 98 | CAUTION: To send YAML content with `curl` client, the `--data-binary` option 99 | must be used in favor of `-d, --data` to preserve line breaks. 100 | 101 | [#reference] 102 | == Reference 103 | 104 | include::db:partial$symbols.adoc[leveloffset=+1] 105 | 106 | include::partial$drawing-objects.adoc[leveloffset=+1] 107 | 108 | include::partial$drawing-deftypes.adoc[leveloffset=+1] 109 | -------------------------------------------------------------------------------- /racksdb/generic/dumpers/json.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023 Rackslab 2 | # 3 | # This file is part of RacksDB. 4 | # 5 | # SPDX-License-Identifier: MIT 6 | 7 | from typing import Any 8 | import json 9 | import logging 10 | 11 | from ._common import MapperDumper 12 | from ...generic.db import DBObject, DBObjectRange, DBObjectRangeId, DBDict, DBList 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | class GenericJSONEncoder(json.JSONEncoder, MapperDumper): 18 | def __init__(self, objects_map={}, fold=True, **kwargs): 19 | json.JSONEncoder.__init__(self, **kwargs) 20 | MapperDumper.__init__(self, objects_map) 21 | self.fold = fold 22 | 23 | def _fill_obj_dict(self, result, obj, prop, value): 24 | value = self.map(obj, prop, value) 25 | if value is None: 26 | return 27 | if ( 28 | isinstance(value, DBObject) 29 | or isinstance(value, DBDict) 30 | or isinstance(value, DBList) 31 | ): 32 | result[prop] = self.default(value) 33 | else: 34 | result[prop] = value 35 | 36 | def default(self, obj: Any) -> Any: 37 | if isinstance(obj, DBObjectRange): 38 | return str(obj.rangeset) 39 | elif isinstance(obj, DBObjectRangeId): 40 | return obj.start 41 | elif isinstance(obj, DBDict): 42 | if self.fold: 43 | # Force iteration over the values of the dictionnary to avoid automatic 44 | # expansion performed by DBDict iterator. 45 | return [item for item in obj.values()] 46 | else: 47 | # Use DBDict iterator to expand potential DBExpandableObject. 48 | return [item for item in obj] 49 | elif isinstance(obj, DBList): 50 | if self.fold: 51 | return [item for item in obj.itervalues()] 52 | else: 53 | # As a DBList is also a standard iterable Python list, json.JSONEncoder 54 | # will iterate itself over the DBList, thus expanding potential 55 | # expandable objects automatically. There is nothing more to do in this 56 | # case. 57 | return obj 58 | elif isinstance(obj, DBObject): 59 | result = {} 60 | for prop in obj._schema.properties: 61 | try: 62 | self._fill_obj_dict(result, obj, prop.name, getattr(obj, prop.name)) 63 | except AttributeError: 64 | continue 65 | return result 66 | # Let the base class default method raise the TypeError 67 | return json.JSONEncoder.default(self, obj) 68 | 69 | 70 | class DBDumperJSON: 71 | def __init__(self, show_types=False, objects_map={}, fold=True): 72 | self.objects_map = objects_map 73 | self.fold = fold 74 | 75 | def dump(self, obj: Any) -> str: 76 | # DBDict are also standard python dictionnaries, then json.JSONEncoder thinks it 77 | # can handle without hurdle and it does not call GenericJSONEncoder.default() 78 | # for this obj type. However: 79 | # 80 | # - DBDict must be represented as a list of values in dumps, the keys must not 81 | # be represented as they are just defined for pratical reasons to facilitate 82 | # data manipulation in library. 83 | # - Keys can be DBObjectRange objects that cannot be represented by standard 84 | # json.JSONEncoder. 85 | # 86 | # For these reasons, DBDict is converted here as a standard list of values. 87 | if isinstance(obj, DBDict): 88 | if self.fold: 89 | obj = [item for item in obj.values()] 90 | else: 91 | obj = [item for item in obj] 92 | return GenericJSONEncoder(objects_map=self.objects_map, fold=self.fold).encode( 93 | obj 94 | ) 95 | -------------------------------------------------------------------------------- /docs/utils/schema-objs.adoc.j2: -------------------------------------------------------------------------------- 1 | {# 2 | Warn people opening the resulting file it is generated using this template 3 | and the accompanying script. 4 | -#} 5 | //// 6 | Do not modify this file directly, it is automatically generated by combining 7 | the Python script `docs/utils/schema-objs.py` and the template 8 | `docs/utils/schema-objs.adoc.j2`. Please refer to the Python script comments 9 | to discover how it is used. 10 | //// 11 | {# 12 | Macro to represent the type of an object attribute 13 | #} 14 | {%- macro represent_type(data_type) -%} 15 | {%- if data_type.__class__.__name__ == 'SchemaContainerList' -%} 16 | {symbol-seq}{nbsp}{{ represent_type(data_type.content) }} 17 | {%- elif data_type.__class__.__name__ == 'SchemaReference' -%} 18 | {symbol-ref}{nbsp}{{ represent_type(data_type.obj) }}{% if data_type.prop is not none %}.{{data_type.prop}}{% endif %} 19 | {%- elif data_type.__class__.__name__ == 'SchemaBackReference' -%} 20 | {{ represent_type(data_type.obj) }}{% if data_type.prop is not none %}.{{data_type.prop}}{% endif %} 21 | {%- elif data_type.__class__.__name__ == 'SchemaObject' -%} 22 | xref:#obj-{{ data_type.name|lower() }}[{symbol-obj}{nbsp}{{ data_type.name }}] 23 | {%- elif 'SchemaDefinedType' in data_type|bases -%} 24 | xref:#deftype-{{ data_type.name[deftype_prefix|length:]|lower() }}[{symbol-deftype}{nbsp}~{{ data_type.name[deftype_prefix|length:] }}] 25 | {%- else -%} 26 | {{ data_type }} 27 | {%- endif -%} 28 | {%- endmacro -%} 29 | 30 | {%- macro represent_automatic_prop_name(prop) -%} 31 | {%- if prop.type.__class__.__name__ == 'SchemaBackReference' -%} 32 | {symbol-backref} 33 | {%- elif prop.computed -%} 34 | {symbol-computed} 35 | {%- endif -%} 36 | {nbsp}{{ prop.name }} 37 | {%- endmacro -%} 38 | {# 39 | Macro to render obj attributes table 40 | #} 41 | {%- macro obj_attributes(obj) -%} 42 | .Properties 43 | [cols="{tbl-obj-props-cols-specs}"] 44 | |=== 45 | |Property|Description|Type|Required 46 | {% for prop in obj.properties if not prop.type.__class__.__name__ == 'SchemaBackReference' and not prop.computed %} 47 | |{{ prop.name }}{% if prop.key is true %}{nbsp}{symbol-key}{% endif %} 48 | |{{ prop.description }} 49 | {% if prop.default is not none %} 50 | {%- if prop.default is mapping %} 51 | *Default value:* 52 | 53 | [source,yaml] 54 | ---- 55 | {{prop.default|toyaml}} 56 | ---- 57 | {%- else %} 58 | *Default value:* `{{prop.default}}` 59 | {%- endif %} 60 | {%- endif %} 61 | |{{ represent_type(prop.type) }} 62 | |{% if prop.required is false %}[.grey]#-#{% else %}[.green]#icon:check[]#{% endif %} 63 | {% endfor -%} 64 | |=== 65 | {%- endmacro -%} 66 | {# 67 | Macro to render obj backrefs table 68 | #} 69 | {%- macro obj_backrefs(obj) %} 70 | 71 | .Automatic properties 72 | [cols="{tbl-obj-backrefs-cols-specs}"] 73 | |=== 74 | |Property|Description|Type 75 | {% for prop in obj.properties if prop.type.__class__.__name__ == 'SchemaBackReference' or prop.computed %} 76 | |{{ represent_automatic_prop_name(prop) }} 77 | |{{ prop.description }} 78 | |{{ represent_type(prop.type) }} 79 | {% endfor -%} 80 | |=== 81 | {%- endmacro -%} 82 | :tbl-obj-props-cols-specs: 2m,6a,4,^.^1 83 | :tbl-obj-backrefs-cols-specs: 4m,6a,4 84 | 85 | [#obj-root] 86 | == Database Root 87 | 88 | Library class type: `{{object_prefix}}` 89 | 90 | {{ obj_attributes(schema.content) }} 91 | 92 | [#objects] 93 | == Objects 94 | {#- 95 | Iterate over the various objects sorted in lexicographical order. 96 | -#} 97 | {% for obj in schema.objects.values()|sort(attribute='name') %} 98 | 99 | [#obj-{{ obj.name|lower() }}] 100 | === {{ obj.name }} 101 | 102 | Library class type: `{{object_prefix}}{{ obj.name }}` 103 | {% if obj.description is not none %} 104 | {{ obj.description }} 105 | {% endif %} 106 | {{ obj_attributes(obj) }} 107 | {%- if obj.has_backref -%} 108 | {{ obj_backrefs(obj) }} 109 | {%- endif -%} 110 | {%- endfor %} 111 | -------------------------------------------------------------------------------- /docs/modules/overview/pages/overview.adoc: -------------------------------------------------------------------------------- 1 | = Overview 2 | 3 | == Purpose 4 | 5 | *RacksDB* is an _open source_ solution for modeling your datacenters 6 | infrastructures. It provides a simple database schema to store information about 7 | the equipments in your datacenters, with tools and library to request this 8 | database. This database can be used as *reference source* for many purposes in 9 | IT management. 10 | 11 | image::overview:racksdb_overview.png[RacksDB Overview] 12 | 13 | * **Inventory**: Get permanent reference list of equipments charactistics and 14 | enable conformity testing. 15 | * **Automation**: Adopt Infrastructure-as-Code by coupling generic configuration 16 | management logic with infrastructures model. 17 | * **Continuous deployment**: Define advanced deployment rules based on nodes 18 | characteristics declared in RacksDB. 19 | * **Monitoring**: Synchronize monitoring services with equipments database and 20 | define dynamic dashboard. 21 | * **Documentation**: Get updated architecture diagrams and define generic 22 | adaptative procedures. 23 | 24 | :wiki-cmdb: https://en.wikipedia.org/wiki/Configuration_management_database 25 | :wiki-dcim: https://en.wikipedia.org/wiki/Data_center_management#Data_center_infrastructure_management 26 | 27 | Generally speaking, *RacksDB* is a specialized 28 | {wiki-cmdb}[CMDB]footnote:[Configuration management database] dedicated 29 | to {wiki-dcim}[DCIM]footnote:[Data center-infrastructure management]. 30 | 31 | Compared to others products, *RacksDB* can be considered lightweight 32 | alternative to more advanced tools such as https://netbox.dev/[NetBox] and 33 | https://www.racktables.org/[RackTables]. Discover the xref:#characteristics[main 34 | characteristics] that makes it unique among other solutions. 35 | 36 | [#characteristics] 37 | == Characteristics 38 | 39 | The main characteristics of *RacksDB* are the following: 40 | 41 | * **YAML based.** The data are stored in plain files using human-readable YAML 42 | format, easily managed with Git. 43 | * **Simple and pragmatic.** The database schema is intuitive and practical, 44 | infrastructures can be described in few minutes. 45 | * **Tag-based.** Easy filtering of data and equipments based on associated tags. 46 | * **Decentralized architecture.** RacksDB can be deployed without central 47 | server, just by replicating few plain YAML files. 48 | * **Extensibility.** In addition to RacksDB simple format, custom schema 49 | extensions can be defined to store specific data. 50 | 51 | == Interfaces 52 | 53 | image::overview:racksdb_interfaces.png[RacksDB Interfaces] 54 | 55 | Once the database is defined, data can be searched and extracted using the 56 | xref:usage:racksdb.adoc[`racksdb` CLI]. A xref:usage:ui.adoc[Web UI] is also 57 | available to explore its content. 58 | 59 | image::overview:racksdb_web_ui_screenshots.webp[RacksDB Web UI,width=600] 60 | 61 | For full integration with your software, a xref:usage:lib.adoc[Python library] 62 | and a xref:usage:rest.adoc[REST API] can also be used to stay continuously 63 | synchronized with database content. 64 | 65 | [sidebar] 66 | -- 67 | .More links 68 | * xref:usage:racksdb.adoc[`racksdb` command manual] 69 | * xref:usage:ui.adoc[Web UI user guide] with screenshots and features list 70 | * xref:usage:lib.adoc[Python Library reference] documentation with examples 71 | * xref:usage:rest.adoc[REST API reference] documentation with examples 72 | -- 73 | 74 | == Diagrams 75 | 76 | RacksDB can be used to generate diagrams of your datacenter rooms and the racks 77 | with all their equipments, including _axonometric views_ for 3D representations. 78 | 79 | image::overview:racksdb_diagrams.png[RacksDB Diagrams,xref=image$racksdb_diagrams.svg] 80 | 81 | These diagrams can be generated in various formats (PNG, SVG, PDF) and be 82 | customized with many xref:usage:drawparams.adoc[drawing parameters]. 83 | 84 | [sidebar] 85 | -- 86 | .More links 87 | * xref:usage:racksdb.adoc#draw[`racksdb` CLI drawing feature] 88 | * xref:usage:drawparams.adoc[Drawing parameters howto and reference 89 | documentation] 90 | -- 91 | -------------------------------------------------------------------------------- /frontend/src/views/DatacentersView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 106 | 107 |