# 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/