# Synology ## Keyboard Shortcut to Add Photos to an Album ### How to use? > [!info] Works only on desktop browsers. 1. Update the `TARGET_ALBUM` variable with the name of an existing album 2. Open the browser’s developer console 3. Paste the script into the console (a prompt may appear asking to allow pasting) 4. Close the console 5. While browsing photos, press <kbd>.</kbd> to add the currently viewed photo to the specified album ```javascript // Hotkey: press '.' to add the current selection to the album const HOTKEY = "."; // Target album name (has to already exist in the Synology Photos app) const TARGET_ALBUM = "2025-05 – Boston"; function waitForElement(getter, {timeout = 4000} = {}) { return new Promise((resolve, reject) => { // 1. try synchronously first (fast path) const immediate = getter(); if (immediate) return resolve(immediate); // 2. set up an observer that keeps checking until found or timed-out const obs = new MutationObserver(() => { const el = getter(); if (el) { clearTimeout(killTimer); obs.disconnect(); resolve(el); } }); obs.observe(document.body, {childList: true, subtree: true}); const killTimer = setTimeout(() => { obs.disconnect(); reject(new Error("waitForElement timed out")); }, timeout); }); } // Utility: ignore the hotkey if the user is typing in a form control function isTypingInsideEditable(target) { return ( target.isContentEditable || ["INPUT", "TEXTAREA", "SELECT"].includes(target.nodeName) ); } async function addToAlbum(albumName) { try { /* 1. Click “Add to album” in the toolbar */ const addBtn = await waitForElement(() => Array.from(document.querySelectorAll('button, div[role="button"]')) .find(el => /add\s+to\s+album/i.test(el.textContent)) ); addBtn.click(); /* 2. Choose <albumName> in the dialog list */ const albumRow = await waitForElement(() => Array.from(document.querySelectorAll(".synofoto-album-list-content div")) .find(el => el.textContent.trim() === albumName) ); albumRow.click(); /* 3. Hit the OK / Add button */ const okBtn = await waitForElement(() => Array.from( document.querySelectorAll(".synofoto-general-dialog-footer button") ).find(el => /^(ok|add)$/i.test(el.textContent.trim())) ); okBtn.click(); } catch (err) { console.error("[addToAlbum]", err); } } document.addEventListener("keydown", e => { if (e.key === HOTKEY && !isTypingInsideEditable(e.target)) { addToAlbum(TARGET_ALBUM) .then(() => console.log(`[addToAlbum] Added to album: ${TARGET_ALBUM}`)) .catch(err => console.error("[addToAlbum]", err)); } }); ``` ## Setup CSI provider on OpenShift [Synology CSI (tested 53cefcb)](https://github.com/SynologyOpenSource/synology-csi) can provide both block and file storage with snapshot ability. ### 1. Create a Synology user for management Unfortunately, Synology doesn't have an API scope for SAN management and needs to have **full admin privileges**. However, there is a way to restrict it a bit. ![[Pasted image 20240302083038.png|Step 1: Create csi user]] ![[Pasted image 20240302083021.png|Step 2: Add csi user to administrators group]] ![[Pasted image 20240302083224.png|Step 3: Restrict permissions to all Shared folders (don't forget to update if adding a new Shared folder)]] ![[Pasted image 20240302083321.png|Step 4: Restrict access to everything but SMB]] > [!danger] > Don't forget that as it has admin privileges and pretty much unlimited power, anything can happen (k8s compromise, bug in operator). ==Regular backup with HyperBackup is a must!== > ### 2. Enable *iscsid* Service on Workers ```yaml apiVersion: machineconfiguration.openshift.io/v1 kind: MachineConfig metadata: labels: machineconfiguration.openshift.io/role: worker name: worker-iscsi-configuration spec: config: ignition: version: 3.2.0 systemd: units: - name: iscsid.service enabled: true ``` After applying this, all worker nodes will be restarted. ### 3. Install operator and create StorageClasses ```shell git clone https://github.com/SynologyOpenSource/synology-csi && cd synology-csi ``` Create a directory for our custom resources: ```shell mkdir -p deploy/kubernetes/custom ``` Copy these files to `deploy/kubernetes/custom` and update variables `<VAR>` based on your environment: ```yaml # client-info.yml apiVersion: v1 kind: Secret type: Opaque metadata: name: client-info-secret namespace: synology-csi stringData: client-info.yml: | clients: - host: <SYNOLOGY_IP> port: 5001 https: true username: csi password: <PASSWORD> ``` ```yaml # openshift-scc.yml kind: SecurityContextConstraints apiVersion: security.openshift.io/v1 metadata: name: synology-csi allowHostDirVolumePlugin: true allowHostNetwork: true allowPrivilegedContainer: true allowedCapabilities: - 'SYS_ADMIN' defaultAddCapabilities: [] fsGroup: type: RunAsAny groups: [] priority: readOnlyRootFilesystem: false requiredDropCapabilities: [] runAsUser: type: RunAsAny seLinuxContext: type: RunAsAny supplementalGroups: type: RunAsAny users: - system:serviceaccount:synology-csi:csi-controller-sa - system:serviceaccount:synology-csi:csi-node-sa - system:serviceaccount:synology-csi:csi-snapshotter-sa volumes: - '*' ``` ```yaml # synology-iscsi-storage-class.yml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: annotations: storageclass.kubernetes.io/is-default-class: "true" name: synology-iscsi-storage provisioner: csi.san.synology.com parameters: fsType: ext4 dsm: <SYNOLOGY_IP> location: /volume2 # or other volume # formatOptions: '...' # mountOptions: [] reclaimPolicy: Retain # or Retain, Recycle allowVolumeExpansion: true ``` ```yaml # synology-smb-storage-class.yml apiVersion: v1 kind: Secret metadata: name: synology-csi-smb-credentials namespace: synology-csi type: Opaque stringData: username: "csi" password: "<PASSWORD>" --- apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: annotations: storageclass.kubernetes.io/is-default-class: "false" name: synology-smb-storage provisioner: csi.san.synology.com parameters: protocol: smb location: /volume2 # or other volume dsm: <SYNOLOGY_IP> csi.storage.k8s.io/node-stage-secret-name: synology-csi-smb-credentials csi.storage.k8s.io/node-stage-secret-namespace: synology-csi mountOptions: - dir_mode=0770 - file_mode=0770 - uid=0 - gid=0 reclaimPolicy: Delete # or Retain, Recycle allowVolumeExpansion: true ``` ```yaml # synology-snapshot-class.yml apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshotClass metadata: name: synology-snapshot annotations: storageclass.kubernetes.io/is-default-class: "true" driver: csi.san.synology.com deletionPolicy: Delete # parameters: # description: 'Kubernetes CSI' # only for iscsi protocol # is_locked: 'false' ``` > [!warning] > For the sake of simplicity, I used secrets created manually, but the best way is to have them generated by some secret manager (I personally use HashiCorp Vault). Create manifests in this order: ```shell oc create -f deploy/kubernetes/v1.20/namespace.yml oc create -f deploy/kubernetes/v1.20/custom/client-info.yml oc create -f deploy/kubernetes/v1.20/custom/openshift-scc.yml oc create -f deploy/kubernetes/v1.20/controller.yml oc create -f deploy/kubernetes/v1.20/csi-driver.yml oc create -f deploy/kubernetes/v1.20/node.yml oc create -f deploy/kubernetes/v1.20/custom/synology-iscsi-storage-class.yml oc create -f deploy/kubernetes/v1.20/custom/synology-smb-storage-class.yml oc create -f deploy/kubernetes/v1.20/snapshotter/snapshotter.yaml oc create -f deploy/kubernetes/v1.20/custom/synology-snapshot-class.yml ``` ### 4. Testing Let's create a dedicated namespace for testing: ```shell-session oc new-project tmp ``` #### iSCSI volume Create `iscsi-test` volume: ```shell oc create -n tmp -f - <<'EOF' kind: PersistentVolumeClaim apiVersion: v1 metadata: name: iscsi-test spec: storageClassName: synology-iscsi-storage accessModes: - ReadWriteOnce resources: requests: storage: 5G EOF ``` Run a benchmark on `iscsi-test` volume: ```shell oc create -n tmp -f - <<'EOF' apiVersion: batch/v1 kind: Job metadata: name: write-iscsi spec: template: metadata: name: write-iscsi labels: app: speedtest job: write-iscsi spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: write-iscsi image: ubuntu command: ["dd","if=/dev/zero","of=/mnt/pv/test.img","bs=1G","count=1","oflag=dsync"] volumeMounts: - mountPath: "/mnt/pv" name: test-volume securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumes: - name: test-volume persistentVolumeClaim: claimName: iscsi-test restartPolicy: Never EOF oc wait -n tmp --timeout=600s --for=condition=complete job/write-iscsi oc create -n tmp -f - <<'EOF' apiVersion: batch/v1 kind: Job metadata: name: read-iscsi spec: template: metadata: name: read-iscsi labels: app: speedtest job: read-iscsi spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: read-iscsi image: ubuntu command: ["dd","if=/mnt/pv/test.img","of=/dev/null","bs=8k"] volumeMounts: - mountPath: "/mnt/pv" name: test-volume securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumes: - name: test-volume persistentVolumeClaim: claimName: iscsi-test restartPolicy: Never EOF oc wait -n tmp --timeout=600s --for=condition=complete job/read-iscsi ``` > [!info] > OpenShift automatically adds `securityContext.runAsUser` and `securityContext.fsGroup`, so the volume is mounted with the correct GID permissions. #### SMB volume Create `smb-test` volume: ```shell oc create -n tmp -f - <<'EOF' kind: PersistentVolumeClaim apiVersion: v1 metadata: name: smb-test spec: storageClassName: synology-smb-storage accessModes: - ReadWriteOnce resources: requests: storage: 5G EOF ``` Run a benchmark on `smb-volume`: ```shell oc create -f - <<'EOF' apiVersion: batch/v1 kind: Job metadata: name: write-smb spec: template: metadata: name: write-smb labels: app: speedtest job: write-smb spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: write-smb image: ubuntu command: ["dd","if=/dev/zero","of=/mnt/pv/test.img","bs=1G","count=1","oflag=dsync"] volumeMounts: - mountPath: "/mnt/pv" name: test-volume securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumes: - name: test-volume persistentVolumeClaim: claimName: smb-test restartPolicy: Never EOF oc wait -n tmp --timeout=600s --for=condition=complete job/write-smb oc create -n tmp -f - <<'EOF' apiVersion: batch/v1 kind: Job metadata: name: read-smb spec: template: metadata: name: read-smb labels: app: speedtest job: read-smb spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: read-smb image: ubuntu command: ["dd","if=/mnt/pv/test.img","of=/dev/null","bs=8k"] volumeMounts: - mountPath: "/mnt/pv" name: test-volume securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] volumes: - name: test-volume persistentVolumeClaim: claimName: smb-test restartPolicy: Never EOF oc wait -n tmp --timeout=600s --for=condition=complete job/read-smb ``` #### Check logs Check the results: ```shell-session $ oc get po -n tmp NAME READY STATUS RESTARTS AGE read-iscsi-mrncz 0/1 Completed 0 12m read-smb-q4lcn 0/1 Completed 0 25s write-iscsi-5qr2w 0/1 Completed 0 14m write-smb-5mpkj 0/1 Completed 0 4m9s ``` Numbers from my 1 Gbit connection: ```shell-session $ oc logs -l=app=speedtest,job=write-iscsi 1+0 records in 1+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 12.0059 s, 89.4 MB/s $ oc logs -l=app=speedtest,job=read-iscsi 131072+0 records in 131072+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 9.23677 s, 116 MB/s $ oc logs -l=app=speedtest,job=write-smb 1+0 records in 1+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 11.6427 s, 92.2 MB/s $ oc logs -l=app=speedtest,job=read-smb 131072+0 records in 131072+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 9.20358 s, 117 MB/s ``` #### DSM UI ##### iSCSI volume ![[Pasted image 20240302082854.png|SAN Manager - LUNs]] ##### SMB volume ![[Pasted image 20240302075903.png|Samba shared folder]] ![[Pasted image 20240302080337.png|In case of SMB volume, it can be inspected with File Station]] > [!info] > If using `reclaimPolicy: Delete`, operator will delete the corresponding volume after deleting the `PersistentVolumeClaim`. ### 5. Snapshot and Restore #### Snapshot ```shell oc create -n tmp -f - <<'EOF' apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: iscsi-test spec: volumeSnapshotClassName: synology-snapshot source: persistentVolumeClaimName: iscsi-test EOF ``` ```shell-session $ oc get -n tmp volumesnapshot NAME READYTOUSE SOURCEPVC SOURCESNAPSHOTCONTENT RESTORESIZE SNAPSHOTCLASS SNAPSHOTCONTENT CREATIONTIME AGE iscsi-test true iscsi-test 5000000000 synology-snapshot snapcontent-9c40ada3-5485-41ac-bddf-92077f664502 3s 2m30s $ oc get -n tmp volumesnapshotcontent snapcontent-9c40ada3-5485-41ac-bddf-92077f664502 NAME READYTOUSE RESTORESIZE DELETIONPOLICY DRIVER VOLUMESNAPSHOTCLASS VOLUMESNAPSHOT VOLUMESNAPSHOTNAMESPACE AGE snapcontent-9c40ada3-5485-41ac-bddf-92077f664502 true 5000000000 Delete csi.san.synology.com synology-snapshot iscsi-test tmp 4m56s ``` Verify on Synology: ![[Pasted image 20240302120501.png|Snapshot Replication app]] Create some files on volume to verify restore funcionality. #### Restore We need to create new PVC that references our snapshot as datasource (esentially clone). ```shell oc create -n tmp -f - <<'EOF' apiVersion: v1 kind: PersistentVolumeClaim metadata: name: iscsi-test-restored spec: storageClassName: synology-iscsi-storage dataSource: name: iscsi-test kind: VolumeSnapshot apiGroup: snapshot.storage.k8s.io accessModes: - ReadWriteOnce resources: requests: storage: 5G EOF ``` ```shell-session $ oc get pvc iscsi-test Bound pvc-7addad31-8819-4c01-9c95-689670bd06ab 5G RWO synology-iscsi-storage 52m iscsi-test-restored Bound pvc-5592f2c5-d703-4a27-a363-d758c6a35e96 5G RWO synology-iscsi-storage 4s ``` This PVC with the restored version is ready and can be used. As expected, the files that were created after the snapshot were not there. > [!info] > Usually, the Snapshot and Restore functionality is abstracted away. A quite common solution is [Velero](https://velero.io/). ### Resources - https://xphyr.net/post/ocp_syno_csi/ - https://www.redhat.com/en/blog/a-guide-to-openshift-and-uids - https://www.talos.dev/v1.6/kubernetes-guides/configuration/synology-csi/