From e7718ce3566b4e665c4a61a469a30e45734b0a5c Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Thu, 23 Jul 2026 11:07:18 +0900 Subject: [PATCH] feat: add GPU node setup with Docker and NVIDIA support, including configuration files and playbook updates --- ansible/ansible.cfg | 4 + ansible/group_vars/all/vault.yaml | 12 + ansible/host_vars/gpu-node.yaml | 52 +++ ansible/playbooks/setup-gpu-node.yaml | 1 + ansible/roles/docker/tasks/main.yaml | 43 ++ ansible/roles/nvidia/handlers/main.yaml | 9 + ansible/roles/nvidia/tasks/main.yaml | 78 ++++ .../templates/k3s-containerd-config.toml.j2 | 10 + ansible/roles/ollama/handlers/main.yaml | 4 +- ansible/roles/ollama/tasks/main.yaml | 24 +- .../home-services/nvidia-device-plugin.yaml | 48 +++ router/backup-base.tar.gz | Bin 0 -> 8819 bytes router/uci-base.conf | 373 ++++++++++++++++++ 13 files changed, 648 insertions(+), 10 deletions(-) create mode 100644 ansible/ansible.cfg create mode 100644 ansible/group_vars/all/vault.yaml create mode 100644 ansible/host_vars/gpu-node.yaml create mode 100644 ansible/roles/docker/tasks/main.yaml create mode 100644 ansible/roles/nvidia/handlers/main.yaml create mode 100644 ansible/roles/nvidia/tasks/main.yaml create mode 100644 ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2 create mode 100644 manifests/home-services/nvidia-device-plugin.yaml create mode 100644 router/backup-base.tar.gz create mode 100644 router/uci-base.conf diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..7d59817 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,4 @@ +[defaults] +inventory = inventory.yaml +inject_facts_as_vars = False +deprecation_warnings = False \ No newline at end of file diff --git a/ansible/group_vars/all/vault.yaml b/ansible/group_vars/all/vault.yaml new file mode 100644 index 0000000..fa5b478 --- /dev/null +++ b/ansible/group_vars/all/vault.yaml @@ -0,0 +1,12 @@ +$ANSIBLE_VAULT;1.1;AES256 +31386562396163363266666232663537363730663161363165303536383934353136313133333437 +6236323335383962333464336235346532366133363234390a346633623166393834393430393338 +34373562656663623836326539363738326538653434336334363231633962366332663837336264 +3530316332666434360a666263353964386338366539303961616136626139663338393634333661 +37653538303831633837333738623466343637613966613533646663383535326262383766346565 +34636161353830646462366364373562653032636166663732646530366136303138306563366235 +32343761383435303633613137616666363638336366383364316630313537613363356436333966 +33623632373439653232373833376535646337623830616337306664393266633838333736353933 +63633636653635633835303735616533336239636564303931376535393765383137383336306364 +39613361383466383139663731353536346162343863336263336366306136376162663335623230 +623933336133643533363833323437396531 diff --git a/ansible/host_vars/gpu-node.yaml b/ansible/host_vars/gpu-node.yaml new file mode 100644 index 0000000..bec4193 --- /dev/null +++ b/ansible/host_vars/gpu-node.yaml @@ -0,0 +1,52 @@ +# Host vars for: gpu-node (GPU workstation — spot K3s agent) +ansible_python_interpreter: /usr/bin/python3.12 + +# ── common ───────────────────────────────────────────────────────────────────── +timezone: Asia/Tokyo +username: nik + +base_packages: + - curl + - git + - htop + - vim + - wget + - ca-certificates + - gnupg + - lsb-release + - build-essential + +ufw_allowed_ports: + - { port: "430", proto: tcp, comment: "SSH" } + - { port: "11434", proto: tcp, comment: "Ollama API" } + - { port: "61208", proto: tcp, comment: "Glances web UI" } + +data_dirs: [] + +# ── nvidia ───────────────────────────────────────────────────────────────────── +nvidia_driver_version: "570" +cuda_version: "12-8" + +# ── k3s-agent ────────────────────────────────────────────────────────────────── +k3s_server_url: "https://192.168.7.77:6443" +k3s_node_token: "{{ vault_k3s_node_token }}" + +# Check current cluster version with: k3s --version on minisforum +k3s_version: "v1.32.4+k3s1" + +k3s_node_labels: + node-role: gpu + nik4nao.com/node-type: spot + nik4nao.com/gpu: "true" + +k3s_node_taints: + - "spot=true:NoSchedule" + +# ── ollama ───────────────────────────────────────────────────────────────────── +ollama_port: 11434 +ollama_models: + - qwen3:4b +ollama_models_dir: /usr/share/ollama/.ollama/models + +# ── glances ──────────────────────────────────────────────────────────────────── +# no extra vars — uses role defaults \ No newline at end of file diff --git a/ansible/playbooks/setup-gpu-node.yaml b/ansible/playbooks/setup-gpu-node.yaml index 7f96eb6..ae092f1 100644 --- a/ansible/playbooks/setup-gpu-node.yaml +++ b/ansible/playbooks/setup-gpu-node.yaml @@ -6,6 +6,7 @@ roles: - role: common + - role: docker - role: nvidia - role: k3s-agent - role: ollama diff --git a/ansible/roles/docker/tasks/main.yaml b/ansible/roles/docker/tasks/main.yaml new file mode 100644 index 0000000..26dc3eb --- /dev/null +++ b/ansible/roles/docker/tasks/main.yaml @@ -0,0 +1,43 @@ +--- +# Part of role: docker +# Called by: ansible/playbooks/setup-gpu-node.yaml +# Description: Installs Docker CE on Ubuntu, adds user to docker group. + +- name: Add Docker GPG key + ansible.builtin.shell: + cmd: > + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | + gpg --dearmor -o /usr/share/keyrings/docker.gpg + creates: /usr/share/keyrings/docker.gpg + +- name: Add Docker apt repository + ansible.builtin.apt_repository: + repo: > + deb [arch=amd64 signed-by=/usr/share/keyrings/docker.gpg] + https://download.docker.com/linux/ubuntu + {{ ansible_facts['distribution_release'] }} stable + filename: docker + state: present + +- name: Install Docker CE + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + state: present + update_cache: true + +- name: Ensure Docker service is running and enabled + ansible.builtin.systemd: + name: docker + state: started + enabled: true + +- name: Add user to docker group + ansible.builtin.user: + name: "{{ username }}" + groups: docker + append: true \ No newline at end of file diff --git a/ansible/roles/nvidia/handlers/main.yaml b/ansible/roles/nvidia/handlers/main.yaml new file mode 100644 index 0000000..0bbf08a --- /dev/null +++ b/ansible/roles/nvidia/handlers/main.yaml @@ -0,0 +1,9 @@ +--- +# Part of role: nvidia +# Called by: ansible/playbooks/setup-gpu-node.yaml +# Description: Restarts Docker after nvidia-container-toolkit runtime configuration. +- name: restart docker + ansible.builtin.systemd: + name: docker + state: restarted + daemon_reload: true \ No newline at end of file diff --git a/ansible/roles/nvidia/tasks/main.yaml b/ansible/roles/nvidia/tasks/main.yaml new file mode 100644 index 0000000..0e26d04 --- /dev/null +++ b/ansible/roles/nvidia/tasks/main.yaml @@ -0,0 +1,78 @@ +--- +# Part of role: nvidia +# Called by: ansible/playbooks/setup-gpu-node.yaml +# Description: Installs NVIDIA drivers, CUDA toolkit, and nvidia-container-toolkit. +# Configures Docker and K3s containerd runtimes for GPU access. + +- name: Add NVIDIA CUDA apt keyring + ansible.builtin.shell: + cmd: > + curl -fsSL + https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb + -o /tmp/cuda-keyring.deb && dpkg -i /tmp/cuda-keyring.deb + creates: /usr/share/keyrings/cuda-archive-keyring.gpg + +- name: Add NVIDIA container toolkit keyring + ansible.builtin.shell: + cmd: > + curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | + gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg + creates: /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg + +- name: Add NVIDIA container toolkit repo + ansible.builtin.shell: + cmd: > + curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | + sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | + tee /etc/apt/sources.list.d/nvidia-container-toolkit.list + creates: /etc/apt/sources.list.d/nvidia-container-toolkit.list + +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + +- name: Install NVIDIA driver + ansible.builtin.apt: + name: "nvidia-driver-{{ nvidia_driver_version }}" + state: present + register: nvidia_driver_install + +- name: Install CUDA toolkit + ansible.builtin.apt: + name: "cuda-toolkit-{{ cuda_version }}" + state: present + +- name: Install nvidia-container-toolkit + ansible.builtin.apt: + name: nvidia-container-toolkit + state: present + +- name: Add CUDA to system PATH + ansible.builtin.copy: + dest: /etc/profile.d/cuda.sh + content: | + export PATH=/usr/local/cuda/bin:$PATH + export LD_LIBRARY_PATH=/usr/local/cuda/lib64:$LD_LIBRARY_PATH + mode: "0644" + +- name: Configure Docker runtime for NVIDIA + ansible.builtin.command: + cmd: nvidia-ctk runtime configure --runtime=docker + changed_when: true + notify: restart docker + +- name: Reboot if driver was just installed + ansible.builtin.reboot: + reboot_timeout: 300 + when: nvidia_driver_install.changed + +- name: Verify NVIDIA driver loaded + ansible.builtin.command: nvidia-smi + changed_when: false + failed_when: false + register: nvidia_smi_check + +- name: Assert nvidia-smi succeeded + ansible.builtin.assert: + that: nvidia_smi_check.rc == 0 + fail_msg: "nvidia-smi failed — driver may not have loaded correctly" \ No newline at end of file diff --git a/ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2 b/ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2 new file mode 100644 index 0000000..b36cc43 --- /dev/null +++ b/ansible/roles/nvidia/templates/k3s-containerd-config.toml.j2 @@ -0,0 +1,10 @@ +# Managed by Ansible — do not edit manually +# Adds nvidia as an additional runtime — pods request it via runtimeClassName: nvidia +# runc remains the default runtime for all other workloads +version = 2 + +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia] + runtime_type = "io.containerd.runc.v2" + +[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options] + BinaryName = "/usr/bin/nvidia-container-runtime" \ No newline at end of file diff --git a/ansible/roles/ollama/handlers/main.yaml b/ansible/roles/ollama/handlers/main.yaml index 195b14f..fd72776 100644 --- a/ansible/roles/ollama/handlers/main.yaml +++ b/ansible/roles/ollama/handlers/main.yaml @@ -6,7 +6,7 @@ - name: restart ollama become: true command: launchctl kickstart -k system/com.ollama.ollama - when: ansible_system == 'Darwin' + when: ansible_facts['system'] == 'Darwin' - name: restart ollama linux ansible.builtin.systemd: @@ -14,4 +14,4 @@ state: restarted daemon_reload: true become: true - when: ansible_system == 'Linux' \ No newline at end of file + when: ansible_facts['system'] == 'Linux' \ No newline at end of file diff --git a/ansible/roles/ollama/tasks/main.yaml b/ansible/roles/ollama/tasks/main.yaml index 1c924c2..8eaf907 100644 --- a/ansible/roles/ollama/tasks/main.yaml +++ b/ansible/roles/ollama/tasks/main.yaml @@ -10,7 +10,7 @@ community.general.homebrew: name: ollama state: present - when: ansible_system == 'Darwin' + when: ansible_facts['system'] == 'Darwin' - name: Deploy ollama launchd plist template: @@ -21,7 +21,7 @@ mode: "0644" become: true notify: restart ollama - when: ansible_system == 'Darwin' + when: ansible_facts['system'] == 'Darwin' - name: Load ollama launchd service become: true @@ -29,14 +29,22 @@ args: creates: /var/run/ollama.pid ignore_errors: true - when: ansible_system == 'Darwin' + when: ansible_facts['system'] == 'Darwin' # ── Linux ────────────────────────────────────────────────────────────────────── - name: Install ollama via install script ansible.builtin.shell: cmd: curl -fsSL https://ollama.com/install.sh | sh - creates: /usr/local/bin/ollama - when: ansible_system == 'Linux' + creates: /etc/systemd/system/ollama.service + when: ansible_facts['system'] == 'Linux' + +- name: Create ollama systemd override directory + ansible.builtin.file: + path: /etc/systemd/system/ollama.service.d + state: directory + mode: "0755" + become: true + when: ansible_facts['system'] == 'Linux' - name: Deploy ollama systemd override ansible.builtin.template: @@ -47,7 +55,7 @@ mode: "0644" become: true notify: restart ollama linux - when: ansible_system == 'Linux' + when: ansible_facts['system'] == 'Linux' - name: Enable and start ollama service ansible.builtin.systemd: @@ -56,7 +64,7 @@ enabled: true daemon_reload: true become: true - when: ansible_system == 'Linux' + when: ansible_facts['system'] == 'Linux' # ── shared ───────────────────────────────────────────────────────────────────── - name: Wait for ollama to be ready @@ -76,7 +84,7 @@ - name: Pull ollama models command: > - {{ '/opt/homebrew/bin/ollama' if ansible_system == 'Darwin' else '/usr/local/bin/ollama' }} + {{ '/opt/homebrew/bin/ollama' if ansible_facts['system'] == 'Darwin' else '/usr/local/bin/ollama' }} pull {{ item }} loop: "{{ ollama_models }}" when: item not in (ollama_tags.json.models | map(attribute='name') | list) diff --git a/manifests/home-services/nvidia-device-plugin.yaml b/manifests/home-services/nvidia-device-plugin.yaml new file mode 100644 index 0000000..df2873b --- /dev/null +++ b/manifests/home-services/nvidia-device-plugin.yaml @@ -0,0 +1,48 @@ +# Config for: NVIDIA GPU device plugin +# Applied by: kubectl apply -f manifests/home-services/nvidia-device-plugin.yaml +# Description: Exposes nvidia.com/gpu resource on gpu-node so K3s can schedule GPU workloads. +--- +apiVersion: node.k8s.io/v1 +kind: RuntimeClass +metadata: + name: nvidia +handler: nvidia +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: nvidia-device-plugin + namespace: kube-system +spec: + selector: + matchLabels: + app: nvidia-device-plugin + template: + metadata: + labels: + app: nvidia-device-plugin + spec: + runtimeClassName: nvidia + tolerations: + - key: spot + operator: Equal + value: "true" + effect: NoSchedule + nodeSelector: + nik4nao.com/gpu: "true" + containers: + - name: nvidia-device-plugin + image: nvcr.io/nvidia/k8s-device-plugin:v0.17.0 + args: + - --device-discovery-strategy=nvml + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: [ALL] + volumeMounts: + - name: device-plugin + mountPath: /var/lib/kubelet/device-plugins + volumes: + - name: device-plugin + hostPath: + path: /var/lib/kubelet/device-plugins \ No newline at end of file diff --git a/router/backup-base.tar.gz b/router/backup-base.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..cad43f9de6d3e6b8d420647bbfa74f894cd561a5 GIT binary patch literal 8819 zcmV-(B8=T1iwFP!000001MEEslpMu%t6;!8Bw!9B%;lCA7$KOMzUI(iK&#!Aw4jwX zyAl$I8EU#~W?I|R-E?*D>_~<wO!XW)I(7#+%;&MwT~%H6>;L+{e*Irnf|eAqB_$-o46s!TML0Gk zD+r`jm_pDTRV0Wy_7PYNF;R4WF;xFzh<9*Np5?fBoMX8TmWzo(w1Z*ywc*_X8(F|) zm=06dt?|k=%l{*V031`S)=;_)`yoIq|B|j{lxzf87Sv=Yqaw16tO;Z|L2WdXe?g3u zt;3>C{-e<3e+qn=>AfbxF4kqAdP@5L`;yYWCGjL@l*p$DaQm^DqS80ngr!kUYDUS zN7b{6mXdN%8cAhzGv!t9R$IbFFW49p@)xIl-c;Uyfs%hYCmEB{h9m#Aa$ox8IgX{~ ze*w!sFLGjA{#yYhagjBY2k6qWjLCW)D4GT#o)CQEn%1*6GE8OEmzq&j3z`K*<43AQ zRRXGlVCqsEno5=An=q|ta>_7u3rbE(om{7eoQ^DCrjyD9BP!4$!78MgAWp3$!Z8DA zGO5Aex~6L|D*>!Vd>A_^NLhhXIXpOhdMc-ziUllX6jou1wU3p9roRCd0t7K(aN&rR zHyrs2JBf(?7U}9JRmx2W6Sjn9-y2bsKJ*KUDoenWYboPd?!F9&Sxgi#v{HF6hO>Bq zuV!EhVDOQnlLQ%;pBrVET5zA8Y1ZO@+u@pBo zD%Q?RPI|01-30a0Qb+|ra>;>*qeYT3D#&7n<$;E$Bst`Etu|jLj4kK-DNtxY0e^Z{ z(@p4~o5<&wZSHTSC>gzO&DE5l28jDn%|LPqXe!w(nZ_eFG9w5JkXJlp8#1tg*iqJEyN_ zaXD4DEho9Vr>AeQl8hJT1#Fv^i+3?gG)d3riG#q(DF|DE8e}Y;@x0B9LhL!yRv`*> zm90F_6HW3kA|++D0-Np?Lm1>?XD2o~J=2Y0G-zR+G}75wZKBr0vl;qc`Z!G&>Nf4# zfc27NH7M;NMSF)94N^8Ey0nvGQr@yt8D*@KBA54}qGc=WH6@Q`B}zkNxn04WS_&Dm z@*~^ApPQ((Og>?uB@`_sQ?8KuN;kRb{YdWF6_eJ*hm zB7_yN0#vMeFxT*aCgVTG^!E%5GL{K48AWP7;&9>7Sg4;UuV$2T302((zce(+HPm8l z9^t#90n$Rq)=UVb9GM|%3%c4WWa)Y+tz>KSiLW69$0WzYRhyj#awtdqI%aL2t&M1U zLG$uMB(b)TiM2S+M7pX-j?+ljY71#l$>!3!-K?Bf8IVVzX(Xm{4&#w6 z>x`yb3~&ZGV2}YN1&?BEl9o&$gcyQa7u}~WALazluBsV+L(kynJTMHbFA>VQxifg4Ww}#p*jT?e z4Lal0QQe=~?s#PzV2kBr?ve!yYK3DWMtp-&7mfOhG+<4k9%r~&VwlH`ggJgzy^eBx z4PV9;ukk5AIbFs2oELXzc5hor*tWc3Srr48ySE& zk|X3XQVE#BF>L;7OfaJYluM03NVS3M#2--Omo!U-*>6I!$R;#0i<`wYXsC@tc{Lry`43W}IA;b^_au=Hf9(i({{(_5*^1CN+H^|PGCU{&5; zM_4Gi@tl+ssa;%X6941ypex7!;v(7qCvfrh{-2hBZ-jdLH*~Bfu>erR6uP@fRB9q{ zObo1b95E}qsSF^?CfULf53~3`E|RDSK~~k%K*a;zd+=}ONO>WMlx!;Mj69%ge<0$LJa?j^D&T2Mnnn+wogwb=*$?N!jvqY?_!j0!-Ag&2rE23iVaNff3`|XRVs|f>tbD~hc&cvTn)~I-Bcok zrqjaSv>4liCP!oKNOTQQ`rkApc~aVFw*P|{tIvP%?eiZkg3`Jd^S)vc>cq*(L`Jcx zY}%S@jFUfMl2c>;2y9t6ot@bgt&E7#Lo_TQUX@X;lYNUP87FyJ?yfrsaN^9;Yr#~A zp)<2Bwvz{x{G%eWV15$XXeR&Bc(h{w#d%`?u|ljZ|E+-X`45NrzFnTK=w?(r#p{Ks8{+USJ0(Tx9?-4y>F{Vzn@>;INONyV0!PcNsK zAlcCs$>*iCI>H*1j@2_6ge}A%JI2zAchptt>{x=Zb^lOsZ5S75`6s)&CSwKI%>O5H z@oN3ga_#s}OQ0dli#eLhbrHSqENqp^R=5> zjvTpx*m4!LE1p=HkwT$B70*cAEH069kE{9Y!8{mCd7FZ8J%T?2_g-_)9-^v4Y7!*J z7o5`clQ)&M= z<&FbILhe~{AI(y(ZxvD=%d|rH!m}c^5`a=lvQ2`;V2F=WPCiMuHOs`Lzu*Gj8AIYH z7{=qGkb|c0l$+#i>cWc>cXt(CQ$CNG@2prBVe@>c`Gq_vPU-&w7C056eZKCc2AbG^ z9M8q7^}i71+WNm0P!i*Uk|CD>xh`}Q$cj#D21jV~KuVCq`m`gD1eXmJSQ*{SN02T> zxP;5pFsYCda=fy~LQX|7r5wPjSM}+coaNs2Fu#wdR{suaitF+(1&OLLY=pU|469-) zZYy-$86gsMD(iuYNh_H}_c2J$i~Hid71u*!B69mq#M8KHGDho>QHa%}V`6vc$TgrN z*K#_3-qP^J1K++F?}~#JP{xKpal$5oX8vC;7BBmMd66ae|L{WF{%;8&lnX(bk4Cv! zOgDxeVc`nU!>Jea_Hzpt_eDus|H7Vbwm7fA4li1~T~mX>zcmupFUZ0&tcP^ z7s995H~rzvb*C-?-?!!>9k@_}sUCxOVu^{!gB{Vd;bEH?B<0+Mc-R_Jem`JNJ_Z z2f0hm{T-A)mghY`ZCT`#+*@E};Dx^2x=rB2ZG8jBu0QCc-~4*nl!dQ^pA_D@X4&A& zuWb1Ap4+w`6}$KU|{9cV6*CC$!QDWC2#`d8fe75H5508qU`0k1g zKM!wXzIV$NSKsjEuRT4#RCLKbf9*VoO(S^aZQ$ls7&KuHtVz z=z@ECR$X+~!1PnknR~W1|Lm=Ie=zv+hLh*d?|sdl`rEfJJNV-17v9;mZqsj~|8w|tC*D7&bNORmo%Ym2Tb{UjRq+=`Uotpj zP@VtoZOUDjkG<(2v3|!R*_U<}ZW(-ZbRc!dtKZl-bpD4o6?UGrbnDj+TK@2#ob#hY zXPo`eb(gQceccBKzWB%+$34aW?>kd=y!xeS+fG{c3**zNpIy84`TJ7ee)-RrzHs#~ zHx_<%%#0=7R}DXR{gyS4U-z8@bLn6AZhP{HWB%c`dsn`{_Fo@+@4vV79XoB>Q=1~` z*UmrrcYhN+`EwS~$pq(ULLmh;_mVj!I|*$x@&B=`5Uq;;^6~cm@791n!ppE@EaQc5 z8K%8T`~MF_{p!ypmlNV`|4&PRbSpWQWRn3Iz`U;EXD%54avnd3 z$v_?`Dt=1fYz3&gP0I4PFtBAsC(pBzfkL4Wl7R*As~{u;8Oy*=(P%P2_WvSMA&bB8 zJjl)~m-OBOn(hofoEnQp1>B(Ke9iGf5%WLTbjKoWtfYPt-Om&lhA-TQ0WqPhP8k$*)~EDNM3t&QgL&sWQTtbPA$Yap3K zMMMTbGEy1}FismQbt-Q=MfPAIEY=(w zk}yT|e8}BN8Ip29(NGu{+50w{@So>m)$6~wAhhFuErG5sW^oRdFDnt4~)3yxxkR)ClR&bOp6v?7^;$T*{l%zqXfvzS6ok#g=Gs|Xtq z8Eidh61nAyoWYP8TnP%uP*MT0Dv0Vgbi-DGRog1#Mk;O7IolC#Ym|Ke=kmIOO_uan zgNN`WYmrOpP;LU-ATl10MjOe$80V|kf1=R7|DiR|H7!EUpCN3+Gks$qZ(xa~1_JO- z-035ZHL+NCuKy>(nAe>PTO?uwjzSZ;$AVquK~%}il~F*!Ih*|JUBp z#>R0}@(5~!#@;1B-*HB?kk2_aM(5CqgHh$5&ch^hhvRU}$a5lH-lf91WI+0RStG}vbW zH;HWT?97{wH}m$rd2imMOD{T`GiIZB@i0nhP+%d_h6lBhqUS*PDEZ)dgH5PL#){6B zbybs)-HCcf;EP-eP44hCF~cs?(PsJ?b;9u>4CG(+!Z~!`tME&Lvs&qbrZGZ?CtuW8 zBaC0qR!DIkL$cv4pu1Q*>M5mVj4pbn6r`tOgpiReeZ{&!vN*<0$Wcl}Mi#sy9>jh|`{L6STZMxys58ulVV$U23{dHhZ)C*pNPg+^qg&hAOr2RDpvkf}HU zq6JJEfWjCtokUSpga$qKy7{aQS4?&SJF8~^RHc*3AVwiqp^;1g6pq&?w6ORyFx6q0 z;T*aiqF)(P+m&FPb9Ee9kAGd1B-w0)?>!x13TWIYv4&`$ESQBHEC|iQxv0{ffjODz>b{do-l1`5HPYx%Y zAb{*dmj1yW;m1-~6g|-pN6F99WpQ_c@VlZ$?m@tx=f)gM$TDAK@?guMcU=+lC%I3LmaM1u3TZ; zJSMcxUDAv9c!hxsh}h5GsN^1CVc2%<$lN7S$m%|{b^%&h5LNW=D=`EiPL&E>rt=Em zTOm{F0iv3S`#j#fh2{V>yf>K_Qj_g7M5m@$I_x&pVB`v+<{9(Ga8J2m>5!;c}E*p_a(U~7seK9aot@egYF2tUEsI)RwZ(%X8xmb+a z*j!u!{TvGp{;TT>-e&B9&|dr7vR~b`HKVBGc~c&b&~QWFwH{krw5I&AMIqO(^=fdJ zEG;Mh$9vuQpX~m3EZ);q=l^Sw9Q!XhLH~@hpC}V3zzJ}pu#Q9w{_zL&3&~1QQbG;B zI?&(?{^Q?KbkGr{f4PNh^REtS{^cfb)#=4v6{x=O2yjHD^W%hrp zw>v2R$NJ>D{YOnwxqyzmhWbZF)BQs!;WUvn#;r!EFsfuVa)vzpG|Fr!UfKndF#*eF z8TUVnJ_t-!FatcEClgB{k84*HS*c*%Kx+XY&RYQ)m^o7`5^2bK4n!|vfLMV3_D!QR zOmHPb!12oNHUouC;BJGN5&6s$A|k!0!q#%IW#ns?vhZp(RtJa zEpL%*83k`tQ?YJjEs%f&hgSyv3lG$#Bjckdw+rMbjnPife+_)h@l(E_@ncse*l;M( zakm?UMOpBIV{FsM(jtSRi!ay`A&o^L|3i$zkg`5EZNSP(Gw6w z_kVsCyd3}6(-*Y=>&2`({jWiC=xGY+=~>GCCBlSnK1DSytPmj;(9Q%^Nfs#p@q>0; zKj5mC1zA>Q=YM#pL%q-d!JFbIlp-TFqJ1<-4GT0pp9BFH#bg_lKtsftDm%L^n%V+A zE-Sd6O$gd#P@_aE@*ot|A*gGOxs^%&{jU_yOPP5PajkhxTY;U=(sKM?ygO+B74PY; z>wjyK*jMl(vDpAz0`6zfx97Q6C$99#Rfu&#FTrY2?IS!9>WVZ>IG$I)K80WAcC2~y z@Ii8bZ(TR_A3aP`&z1EBC5O9|ra_GzfCG>9K>SW{x3VBz_F4UYczA}kgTutbVmF77 z5nf8M$G~fIPR&ykYpYt$S9^k$3RbpHr`&{?(_x8r?^uH#_mdZxblPG#5 zne5Nk!3;)Qymq_&j0H6Keb6a$kB3%X>{4e4_m~t z2o=|;&X(IO%xVO$bd{MJID_4_|HN%X)c6c1g>XXeXMYv_E}-r?ejOSzDF z@mS0l={-@2_H-|#hqb-2qA@mpu=|`^*b|ozjUPL9K0bE9njIS*NOZ~ZJ-rDzu@_UK zFOf*NibL6-&ybgMgTH~YEiS2(ams>># z5X*i4VZR5z|M9N6|A(3+N7KL79jT`^Nv@J@_1ai&|Myj$|Hk4yb^gC5i5>nT5um4I zXV3`Ee<1AUD>+@U=qXwfwS$7c+=8BGnoU-0K~G|iQw<&s#@zg|mexK$jG=h*dGm zvY**V%;L^njKSBA8tov8MGpkf z#@o3(NVUDp{$Eq2a?x`ogqpU{n=4qtDw_-X0yA)?_O5k?q$bH9-)wc-SZ4pnWS{?! zKHUG^@w)v-O_F@AF)Ckcc;}_2#)ihm)^&g0ePiE;n|>RYg_eySpBw&Vvbm{Io{4Re zTU(l@np?Iuo;nrVB5%frjcu(?$hFvJ`C-i4(6)g`T8lj)KZZF%+ap|Yyhqy`-_tFt zcG;%34g8J#+MJMpT79slV`ut_WW1B^vWB9 zd&Z#1pKf;k=*qQMJIDXA@qFR6O~3!^7r#3(clGS^w{L&z_}icVRB^4BY(1@D@<*^- zoi>&`|Bdym@F4GqmCwds20gD-5>_0sP5-rUmO^2g9`T9m)YFT6PL pVdM92Z2R#`f=d;z53U4VT