From 5132a4de2f7f91344f206fab875e7dfe061c3bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sascha=20K=C3=B6nig?= Date: Sat, 15 Aug 2026 09:33:28 +0200 Subject: [PATCH] feat: change zammad domain --- flake.lock | 86 ++++++++++++++++-- hosts/AZ-CLD-1/secrets.nix | 12 +++ .../AZ-CLD-1/services/containers/default.nix | 2 +- hosts/AZ-CLD-1/services/containers/homarr.nix | 1 + .../services/containers/zammad-hr.nix | 30 +++++- hosts/AZ-CLD-1/services/pgbouncer.nix | 21 +++-- hosts/AZ-PRM-1/secrets.nix | 10 ++ hosts/AZ-PRM-1/services/default.nix | 1 + hosts/AZ-PRM-1/services/netbox.nix | 79 ++++++++++++++++ hosts/AZ-PRM-1/services/postgres.nix | 3 +- hosts/common/ports.nix | 2 + secrets.nix | 2 + secrets/netbox-api-token-pepper.age | 18 ++++ secrets/netbox-secret-key.age | 18 ++++ secrets/pgbouncer-userlist.age | Bin 2173 -> 3266 bytes 15 files changed, 265 insertions(+), 20 deletions(-) create mode 100644 hosts/AZ-PRM-1/services/netbox.nix create mode 100644 secrets/netbox-api-token-pepper.age create mode 100644 secrets/netbox-secret-key.age diff --git a/flake.lock b/flake.lock index 3b4fcc5..7ac78d8 100644 --- a/flake.lock +++ b/flake.lock @@ -248,16 +248,16 @@ ] }, "locked": { - "lastModified": 1785348116, - "narHash": "sha256-AVycTcomtfvQJ/AHvMg5xu7lVoOzDXxjN+pV7mqUIQ4=", + "lastModified": 1785979955, + "narHash": "sha256-OP96Ak4OBydR98keoNYKdGY8j+wrIys0LafSZgwS20g=", "owner": "basecamp", "repo": "basecamp-cli", - "rev": "e1059fb123a03295ce6176272e4bbaf43d7971c6", + "rev": "61a5591de1da9bc99c56b8072a4d6bad2c9b9e47", "type": "github" }, "original": { "owner": "basecamp", - "ref": "v0.8.0-rc.1", + "ref": "v0.9.0", "repo": "basecamp-cli", "type": "github" } @@ -295,6 +295,29 @@ "type": "github" } }, + "buzz": { + "inputs": { + "nixpkgs": [ + "m3ta-nixpkgs", + "nixpkgs" + ], + "rust-overlay": "rust-overlay", + "treefmt-nix": "treefmt-nix_2" + }, + "locked": { + "lastModified": 1785934673, + "narHash": "sha256-U/hMY9kkcdxt497yfGuT7+ytrRmE+E15KF6Sc6VrbZo=", + "owner": "mulatta", + "repo": "buzz.nix", + "rev": "f4bb77a87f1ec10e5ad51cfe6d0c4fca481c0534", + "type": "github" + }, + "original": { + "owner": "mulatta", + "repo": "buzz.nix", + "type": "github" + } + }, "darwin": { "inputs": { "nixpkgs": [ @@ -740,17 +763,18 @@ "inputs": { "agents": "agents_4", "basecamp": "basecamp_2", + "buzz": "buzz", "nixpkgs": "nixpkgs_7", "nixpkgs-master": "nixpkgs-master_2", "opencode": "opencode_2", "openspec": "openspec_2" }, "locked": { - "lastModified": 1785997099, - "narHash": "sha256-HReUmHC6RCRnrTuuFq0mYeH+7hnmhJCwZtrBUN97Xho=", + "lastModified": 1786563125, + "narHash": "sha256-a6Rom/Tn66AqPipVRkYu8aaQqFkJjbKh9GaaHVo660g=", "ref": "refs/heads/master", - "rev": "e69ce45b76e39e2b8a6f63f94b1d7fafbafc3ba8", - "revCount": 375, + "rev": "af9422c7b5b0c028a7379a329075bef450a9dfb3", + "revCount": 379, "type": "git", "url": "https://code.m3ta.dev/m3tam3re/nixpkgs" }, @@ -847,7 +871,7 @@ "nixpkgs": [ "nixpkgs" ], - "treefmt-nix": "treefmt-nix_2" + "treefmt-nix": "treefmt-nix_3" }, "locked": { "lastModified": 1785356851, @@ -1472,6 +1496,28 @@ "zugferd-service": "zugferd-service" } }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "m3ta-nixpkgs", + "buzz", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785820796, + "narHash": "sha256-sZXy8mzUMi2cOGulhoW4HWAZB6JhXOAx1x8J4auZFWk=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "b6916ba032e02122d6ed3064f40cabe937363d43", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, "systems": { "locked": { "lastModified": 1681028828, @@ -1554,6 +1600,28 @@ } }, "treefmt-nix_2": { + "inputs": { + "nixpkgs": [ + "m3ta-nixpkgs", + "buzz", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1785360170, + "narHash": "sha256-XE1lKgQ3eIO3E7zWryqcRsax+mYXod/5RHBn4YaR9YE=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "d1187f8bc71fb8aab02395869ec3f5c1920f75c0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + }, + "treefmt-nix_3": { "inputs": { "nixpkgs": [ "nixos-anywhere", diff --git a/hosts/AZ-CLD-1/secrets.nix b/hosts/AZ-CLD-1/secrets.nix index bb60efe..f8c650b 100644 --- a/hosts/AZ-CLD-1/secrets.nix +++ b/hosts/AZ-CLD-1/secrets.nix @@ -88,6 +88,18 @@ file = ../../secrets/pgbouncer-userlist.age; owner = "pgbouncer"; }; + pgbouncer-tls-cert = { + file = ../../secrets/server.crt.age; + owner = "pgbouncer"; + group = "pgbouncer"; + mode = "0444"; + }; + pgbouncer-tls-key = { + file = ../../secrets/server.key.age; + owner = "pgbouncer"; + group = "pgbouncer"; + mode = "0400"; + }; vaultwarden-env = { file = ../../secrets/vaultwarden-env.age; }; diff --git a/hosts/AZ-CLD-1/services/containers/default.nix b/hosts/AZ-CLD-1/services/containers/default.nix index 7e4e927..fd01bb9 100644 --- a/hosts/AZ-CLD-1/services/containers/default.nix +++ b/hosts/AZ-CLD-1/services/containers/default.nix @@ -7,7 +7,7 @@ ./litellm.nix ./librechat-dev.nix ./netbird.nix - ./portainer.nix + # ./portainer.nix ./zammad-hr.nix ]; system.activationScripts.createPodmanNetworkWeb = lib.mkAfter '' diff --git a/hosts/AZ-CLD-1/services/containers/homarr.nix b/hosts/AZ-CLD-1/services/containers/homarr.nix index 13679e6..b537243 100644 --- a/hosts/AZ-CLD-1/services/containers/homarr.nix +++ b/hosts/AZ-CLD-1/services/containers/homarr.nix @@ -13,6 +13,7 @@ in { AUTH_OIDC_CLIENT_NAME = "Azure"; AUTH_OIDC_SCOPE_OVERWRITE = "openid email profile"; AUTH_OIDC_GROUPS_ATTRIBUTE = "roles"; + AUTH_OIDC_GROUPS_LOCAL_MANAGEMENT = "true"; }; environmentFiles = [config.age.secrets.homarr-env.path]; ports = ["127.0.0.1:${toString servicePort}:7575"]; diff --git a/hosts/AZ-CLD-1/services/containers/zammad-hr.nix b/hosts/AZ-CLD-1/services/containers/zammad-hr.nix index fab3170..9fadb4b 100644 --- a/hosts/AZ-CLD-1/services/containers/zammad-hr.nix +++ b/hosts/AZ-CLD-1/services/containers/zammad-hr.nix @@ -21,7 +21,9 @@ ipOffset = 40; # Domain-Konfiguration - zammadDomain = "hr-ticket.az-gruppe.com"; + zammadDomain = "tickets.az-gruppe.com"; + # Alte Domain wird per 301 auf zammadDomain weitergeleitet (siehe Traefik-Config unten) + zammadDomainOld = "hr-ticket.az-gruppe.com"; sharedEnvironment = { MEMCACHE_SERVERS = "zammad-memcached:11211"; @@ -29,7 +31,12 @@ POSTGRESQL_HOST = "10.89.0.1"; POSTGRESQL_USER = "zammad_${instanceName}"; POSTGRESQL_PORT = "6433"; - POSTGRESQL_OPTIONS = "?pool=5"; + # pool=50 entspricht dem Zammad-Default (config/database/database.yml). + # Der Scheduler startet mehrere BackgroundServices-Threads gleichzeitig + # (ProcessSessions/Scheduled/Delayed/DelayedAI + Cleanup/fetch); pool=5 + # war zu klein -> ActiveRecord::ConnectionTimeoutError beim Start. + # Wird durch PgBouncer-Session (default_pool_size=30) server-seitig gedeckelt. + POSTGRESQL_OPTIONS = "?pool=50"; REDIS_URL = "redis://zammad-redis:6379"; TZ = "Europe/Berlin"; BACKUP_DIR = "/var/tmp/zammad"; @@ -232,7 +239,7 @@ in { --env POSTGRESQL_HOST=10.89.0.1 \ --env POSTGRESQL_USER=zammad_${instanceName} \ --env POSTGRESQL_PORT=6433 \ - --env POSTGRESQL_OPTIONS='?pool=5' \ + --env POSTGRESQL_OPTIONS='?pool=50' \ --env REDIS_URL=redis://zammad-redis:6379 \ --env TZ=Europe/Berlin \ --env ELASTICSEARCH_ENABLED=true \ @@ -305,6 +312,13 @@ in { }; }; + # Permanenter Redirect von der alten Domain auf die neue + middlewares."${serviceName}-redirect-old-domain".redirectRegex = { + permanent = true; + regex = "^https?://${lib.replaceStrings ["."] ["\\."] zammadDomainOld}/(.*)"; + replacement = "https://${zammadDomain}/\${1}"; + }; + routers.${serviceName} = { rule = "Host(`${zammadDomain}`)"; tls = { @@ -314,5 +328,15 @@ in { entrypoints = "websecure"; middlewares = ["${serviceName}-headers"]; }; + + routers."${serviceName}-old-domain-redirect" = { + rule = "Host(`${zammadDomainOld}`)"; + tls = { + certResolver = "ionos"; + }; + service = serviceName; + entrypoints = "websecure"; + middlewares = ["${serviceName}-redirect-old-domain"]; + }; }; } diff --git a/hosts/AZ-CLD-1/services/pgbouncer.nix b/hosts/AZ-CLD-1/services/pgbouncer.nix index ff9b7db..b568e4f 100644 --- a/hosts/AZ-CLD-1/services/pgbouncer.nix +++ b/hosts/AZ-CLD-1/services/pgbouncer.nix @@ -22,11 +22,17 @@ log_connections = 1; log_disconnections = 1; + client_tls_sslmode = "require"; + client_tls_cert_file = config.age.secrets.pgbouncer-tls-cert.path; + client_tls_key_file = config.age.secrets.pgbouncer-tls-key.path; + client_tls_protocols = "secure"; + server_reset_query = "DISCARD ALL"; server_check_query = "SELECT 1"; server_check_delay = 30; - server_tls_sslmode = "prefer"; + server_tls_sslmode = "require"; + server_tls_protocols = "secure"; }; mkDatabases = dbs: @@ -92,11 +98,13 @@ in { admin_users = ${commonSettings.admin_users} stats_users = ${commonSettings.stats_users} + client_tls_sslmode = ${commonSettings.client_tls_sslmode} + client_tls_cert_file = ${commonSettings.client_tls_cert_file} + client_tls_key_file = ${commonSettings.client_tls_key_file} + client_tls_protocols = ${commonSettings.client_tls_protocols} + max_client_conn = 300 - # session-Mode: 1 Client-Conn = 1 Server-Conn. zammad_hr haelt via mehrere - # Prozesse (rails+scheduler+websocket) real ~15 Conns -> pro (user,db) 15. - # outline/vaultwarden schoepfen das nie aus (eigene Slots je user,db). - default_pool_size = 15 + default_pool_size = 30 min_pool_size = 1 query_wait_timeout = 30 @@ -105,7 +113,8 @@ in { server_reset_query = DISCARD ALL server_check_query = SELECT 1 server_check_delay = 30 - server_tls_sslmode = prefer + server_tls_sslmode = ${commonSettings.server_tls_sslmode} + server_tls_protocols = ${commonSettings.server_tls_protocols} # PID/Socket getrennt von der Haupt-Instanz halten. pidfile = /run/pgbouncer-session/pgbouncer.pid diff --git a/hosts/AZ-PRM-1/secrets.nix b/hosts/AZ-PRM-1/secrets.nix index ef3b6fb..e53441a 100644 --- a/hosts/AZ-PRM-1/secrets.nix +++ b/hosts/AZ-PRM-1/secrets.nix @@ -25,6 +25,16 @@ in { owner = "traefik"; mode = "0400"; }; + netbox-secret-key = { + file = ../../secrets/netbox-secret-key.age; + owner = "netbox"; + mode = "0400"; + }; + netbox-api-token-pepper = { + file = ../../secrets/netbox-api-token-pepper.age; + owner = "netbox"; + mode = "0400"; + }; traefik-env = { file = ../../secrets/traefik-env.age; }; diff --git a/hosts/AZ-PRM-1/services/default.nix b/hosts/AZ-PRM-1/services/default.nix index 3b623c8..043e0b9 100644 --- a/hosts/AZ-PRM-1/services/default.nix +++ b/hosts/AZ-PRM-1/services/default.nix @@ -8,6 +8,7 @@ ./monitoring ./n8n.nix ./netbird.nix + ./netbox.nix ./pgadmin.nix ./phishboard.nix ./postgres.nix diff --git a/hosts/AZ-PRM-1/services/netbox.nix b/hosts/AZ-PRM-1/services/netbox.nix new file mode 100644 index 0000000..72027de --- /dev/null +++ b/hosts/AZ-PRM-1/services/netbox.nix @@ -0,0 +1,79 @@ +{ + config, + pkgs, + ... +}: let + serviceName = "netbox"; + servicePort = config.m3ta.ports.get serviceName; + staticPort = config.m3ta.ports.get "netbox-static"; + hostName = "nb.l.az-gruppe.com"; + staticRoot = "${config.services.netbox.dataDir}/static"; +in { + services.netbox = { + enable = true; + package = pkgs.unstable.netbox; + listenAddress = "127.0.0.1"; + port = servicePort; + secretKeyFile = config.age.secrets.netbox-secret-key.path; + apiTokenPeppersFile = config.age.secrets.netbox-api-token-pepper.path; + + settings = { + ALLOWED_HOSTS = [hostName "localhost" "127.0.0.1"]; + SECURE_SSL_REDIRECT = true; + SESSION_COOKIE_SECURE = true; + CSRF_COOKIE_SECURE = true; + }; + + extraConfig = '' + SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") + ''; + }; + + services.nginx = { + enable = true; + virtualHosts.netbox-static = { + listen = [ + { + addr = "127.0.0.1"; + port = staticPort; + } + ]; + locations."/static/" = { + alias = "${staticRoot}/"; + extraConfig = '' + expires 1h; + access_log off; + ''; + }; + }; + }; + + users.users.nginx.extraGroups = ["netbox"]; + + # Traefik-Routing: zwei Backends (App + Static), ein Host + services.traefik.dynamicConfigOptions.http = { + services = { + ${serviceName}.loadBalancer.servers = [ + {url = "http://127.0.0.1:${toString servicePort}/";} + ]; + "${serviceName}-static".loadBalancer.servers = [ + {url = "http://127.0.0.1:${toString staticPort}/";} + ]; + }; + + routers = { + ${serviceName} = { + rule = "Host(`${hostName}`) && !PathPrefix(`/static`)"; + tls.certResolver = "ionos"; + service = serviceName; + entrypoints = "websecure"; + }; + "${serviceName}-static" = { + rule = "Host(`${hostName}`) && PathPrefix(`/static`)"; + tls.certResolver = "ionos"; + service = "${serviceName}-static"; + entrypoints = "websecure"; + }; + }; + }; +} diff --git a/hosts/AZ-PRM-1/services/postgres.nix b/hosts/AZ-PRM-1/services/postgres.nix index 1caa6d5..d99175e 100644 --- a/hosts/AZ-PRM-1/services/postgres.nix +++ b/hosts/AZ-PRM-1/services/postgres.nix @@ -39,6 +39,7 @@ authentication = pkgs.lib.mkOverride 10 '' # Local connections (Unix socket) local all postgres peer + local netbox netbox peer local n8n n8n scram-sha-256 # Localhost connections (IPv4 and IPv6) @@ -64,7 +65,7 @@ services.postgresqlBackup = { enable = true; startAt = "03:10:00"; - databases = ["baserow" "kestra" "n8n"]; + databases = ["baserow" "kestra" "n8n" "netbox"]; }; networking.firewall = { extraCommands = '' diff --git a/hosts/common/ports.nix b/hosts/common/ports.nix index 4819ea4..be0b688 100644 --- a/hosts/common/ports.nix +++ b/hosts/common/ports.nix @@ -35,6 +35,8 @@ litellm = 4000; n8n = 5678; + netbox = 8001; + netbox-static = 8002; kestra = 5080; kestra-metrics = 5081; zugferd-service = 5060; diff --git a/secrets.nix b/secrets.nix index ea7e5e0..e324003 100644 --- a/secrets.nix +++ b/secrets.nix @@ -33,6 +33,8 @@ in { "secrets/n8n-db.age".publicKeys = systems ++ users; "secrets/n8n-runner-auth-token-cld.age".publicKeys = systems ++ users; "secrets/n8n-runner-auth-token-prm.age".publicKeys = systems ++ users; + "secrets/netbox-secret-key.age".publicKeys = systems ++ users; + "secrets/netbox-api-token-pepper.age".publicKeys = systems ++ users; "secrets/cld-var-backup-sync-ssh-key.age".publicKeys = [AZ-CLD-1] ++ users; "secrets/netbird-auth-secret.age".publicKeys = systems ++ users; "secrets/netbird-db-password.age".publicKeys = systems ++ users; diff --git a/secrets/netbox-api-token-pepper.age b/secrets/netbox-api-token-pepper.age new file mode 100644 index 0000000..1263d06 --- /dev/null +++ b/secrets/netbox-api-token-pepper.age @@ -0,0 +1,18 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFpoVnNlZyA5c3BQ +MEJKRHFldmszVy9XMDB6eDhUemhHOEI2K2JaekVOZGtuRHcrRkZVCjYzaEladkRD +cGJtZnl3aVN6YnZ4d0l2d3JHRittb2hQNW0yeURCTStod3MKLT4gc3NoLWVkMjU1 +MTkgU3JIYXFBIFRqd05Tckc1NGtSek1ia2NKejIwWkl5OExWY09wY3VTRE1TeE9K +dTlXaTgKbFBkbklKLzVJZ0dQOU1semNjNU45bk0zekRReFIvcmhVV2NjTnJNSXZ0 +UQotPiBzc2gtZWQyNTUxOSBsR3FWWmcgcitFcHJvTm1Uc2Q5UWFad1I0cGlCZWQv +MmlqdGZDbzhPQmtreVo2L3pSdwowS2xTcWhhWUs1WDExK1NteXRPNzVLM2RwZ3dh +bDBZeml4MGlkSktITzF3Ci0+IHNzaC1lZDI1NTE5IENTTXloZyByTytWN3A5Z3Jt +aXE1cmEwQlYzaDM3QjJzZ0tYSjFralc4dzE1MjByb1VjCnpra3pqNEZXS3JuOHBq +NWhKWXFadDRPNSsyVWxKVWxURXVCbzlVSlVmNkEKLT4gfD8laiIpLWdyZWFzZSBT +SVRJIFp9JF9xClY2MVFTenQ5TXBsczQwVHZhUU5BSnR1R1RCWmU4T1pWTEU1Vnhx +TmxyL2Zmb1p5VXNscVBFcm12NHo2N0IyRkQKdk9QWW01OXg1UEN4UnJpWXF4ckha +TVI3WU9nd1dnMkpFODN1SEcwejE4Z1hXSGpFdGJxb1R1N3J3UQotLS0ga2ZReEJV +cVN5dW5wK09tdzJVQjVsZ25GYU0xcndlMHloNFNuZFI2T1BXRQoKJ6ftKtkdC473 +uLbkeR2haQ2HnNtrJIRo1hBrhToR4YkGnc3uCFdNX/xuCxAMznR88Np2rja9LSjk +/y0Kzqpm+5oUcgoW5rHyy3ur3h06HkZ+jPkZAWSMFCPuemTmeYy+HLT0 +-----END AGE ENCRYPTED FILE----- diff --git a/secrets/netbox-secret-key.age b/secrets/netbox-secret-key.age new file mode 100644 index 0000000..c31ec44 --- /dev/null +++ b/secrets/netbox-secret-key.age @@ -0,0 +1,18 @@ +-----BEGIN AGE ENCRYPTED FILE----- +YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IFpoVnNlZyA4NGp1 +a0pOS2xOMngwSkw3VmJKSkc4UnMvRXp1SElxLzFpaVo1M1hYSkJBCnBPTFk0Yk5y +dlp4Z3Q5aGkwYTFXRms5RWJsZFhiMmhPRnVET2Q3K2xBZVUKLT4gc3NoLWVkMjU1 +MTkgU3JIYXFBIHNJRkF3OVF0U0thQjRScGNvdGRmSFc2WWIvMkdpY1BmT1d5YldS +S3VPbDQKN0VLY3U4Z3dPbmt0OFRlb0VpR3RzZEFvTVdVWmduVkladEgyUDNtVmZk +awotPiBzc2gtZWQyNTUxOSBsR3FWWmcgM0swTjBuUzFYOXZCd29wc0ZIZHVySS9i +d3lmWllaL1prNWZzNXBBbi9tVQpoUit0K0lDRkErQzVpa21RSXRBRGo1aFJBbU1Q +bmJTMS9uYjUxMmljMmVBCi0+IHNzaC1lZDI1NTE5IENTTXloZyA2WXhxWVgyYWRl +VytNMzVPdFZSY252V0Y2TnA2NzA2QzVtNEJpN3BGcXdvCmN3RWFCN1FibStuWjFy +QjhQQWpwcGFkOFVaWWJwQzJoZ3NXbHNYT2NHQ2cKLT4gI2JmNHUtZ3JlYXNlIDMK +RFpVaHh2dEhBRjZBam4wV2xOd3d3Zk4vc1lYRGhoaU85b1Y2alZkTDlZWWIzTjNI +aExFUzl5OHJoLy9CK3FQbAp0ME9mS21iRThlZjNTV0hTOU1UUnppam50Vk5YRlp2 +WCtya1MxSFBWCi0tLSBLSnhkUVYwQmNmRzBjdXd4WFFueG53eWkwNVdRWmpuMk9i +cFcwM0tvcWRFCgVyq4v+1nfCjI7R7rsKRI7jsTluy376hFPgjZULUYAsAiWBV400 +R+YoFO+zsem78tNo2xzfepwyhtjAscMWOyACM0tPlhW4k2LVxp2SGJzy1xJ5H+Fz +GGC4Jb5UW+8LQ/0= +-----END AGE ENCRYPTED FILE----- diff --git a/secrets/pgbouncer-userlist.age b/secrets/pgbouncer-userlist.age index 4bcb2f67e0385fcd4a642a0d1322ab9e04471770..8f6a6ed15bb93e5fdd3737713f096a7f868ef6da 100644 GIT binary patch literal 3266 zcmdM|0Vh{?Pd^1mcUJ{hKj)yxfDl&~1vgJ0SCE`8S7dlfj!$@4X{522X|hL9L6VV4 zX_B#5o{w>4g>$BXwx@?*Rid+DPLzwMpMxt-7 zn|EZcmm8N~XmUlkd#H(TxL-kvfu(n%v0sI+hih4iK}BYGre$G}TcmNMp;u_2VY;!O zfvdMqh)H^~v0uJVcv!Y?R;VGDZ%B4}sIix4WQ3cOr&~&>Z&-$}pPO4yn1N||n2Tps zNM5p0XsBUjYN)A6P;N?icy?G(O0Gdwl1G$JVt7~-S87&9b|}bXgP?$vs+5YLoC1?1 zL&wT6SFilUDnCEp(DcgCurRk2x3G#Nm!PcR@RW=cgA{MS?67h_1Jiu2u)zG1fJ~>V zWTW(wsPMo_zmU)h|6r%$AY-@i@Z99|Kx4O}WaFp^zwDgQpop-{z^vq=WCORVV1qC} zUxSQ9E?>8l${@EO{}5wW)2OT{=Oh=u@Z=E3N0Io#Cu$-Xm9MfdC(4tgVi;7^kC_k*x=js;{5|NW1Rq13F zX&h#pm={)<9PZ}JpHy4wLRL_t+?cyl6(CkPjuEMB_ z;^1^63&%)bk0e9CEW;{`bnRqc&$Q%x$8S= zwA@@zQ!oAE+{*NbQpX&Ve7D5>6tBFf^Z=s@&z#UOSJQkhCr8WB5+ko*&k&cK)S{$R z=dA2pb4!!3?A$ z%LxAz)1WjTGef^1W6$EUB4giFbH8wl9K+OPg9?+hN+(0lRLfv9S62%I(!$B;)t@a(!lJDlJv?@G$KX zeM{fSqV&WF<5c$?*K%j)q+H)ji=5Q(%u@4;*R5=%D|%fLwU@Di7z z3IlhyV*LW2%5ZN}Gb5vPi|j`L#vFeBrr{0!}~@N#Dt=PG}1$Mg{M(m?Mr zqoNA!svP5z(xgD2v{EA{mqf4Ph)S-ALet39A`i!+#PC4(D)aQj>>Lw=bYCa4)XGBN zG{bO9)9@m%{Bn06?aDk~M~nP)-*i{6DDMJe!*G+N@~S*8Z7+Y*EZ?xIO248+6Gzu< zM-S~{7axPd5D!DANRy;lZTv%LQ z66_h~TJ9SZ|jkfCp+pKexR zP-&5oom7@lUYzG!l3SSOT4Gk1V^N-&7ZOq8QIVFM%EdFLRvu7n;$#pSQflPoUsjx6;pA&pUXtZg8ldlLmRDBk=BXX( zl4+2b?h+coWmJ^nm10>G8Di|2?BiZiVCs|MtL>7W7f z6qx0nSn6Am>a88&;+I$M7#Wyq#AT5hYT{CmSm<4rRF!PvWm)2%RS~T1>|UB;7G;uN zqHPqOSC#KnkZN9D5am`JSzZxf7U}1k793R-Xcm>7QN-o%=TecK<(rb6Qc@a{RuY_- zQI%JemuTc?=u{Tuo|0=`VXPevO5|?Y!Fk1rmEP{gjv>zG`jKIt;Yr3B#$3TU`Njs8 zl@{4~Ii3dDu90a;zKNx2C7~s$dG7v}<&};hVIjV*g+cxm6@ise$>l*gp%n#|l_}0) z!8yiRX$D*+k$%SB22S3Q+OAP0zQ!pTxke#okpY$-Ccz~JrjFV{j*&SP5mk{+iLN1m znGwN`xrvSehNf8&5&ju@NyR~2o?e~-z7b_67LG2?PCilIzFvtYAsLxbUcO1jmCosb z9$xMiIi5ukX+EKOl_^=S8KFr|6>ep@#l@u+hJnUTT#;pYK~aGzRaI4$xv8c_A(7!h z;bx%)=AM3j$rYth?q%g6CWT&x6<#joiQeX+Sy`dZt|s~}?m6z>873LU$y}C+h7s;g zg(gAfxz4HPo*9{G<;GrSDUpGO<=M{W!A_2Oo>jidk)D1Z;n@aB8NLyr`A+)&UiuMSzC~`vWsyFnQ9eH5DS?%K7Kvu*i9Y3-DQSUOCTV6~ ze*PX7e!1xtk&ywGRTcqxxuynL75-JF`L30zc@g>+T*b*%u11lW$-zZ_7EWFU`W9Xx z*(P40-bJB-*~Vdsxt7`HB`%gp#ujdYW$7W#mQfY@IawaYmZqMjsd>gxZd_S88O6cQ z;chwFWoEAWCds}zhEe+Z zjv-a~jzL_(h5>FKkyT+O9x3^`q1nMc9!4P+MTzyl>f7!kn*?t{4cx!~-c006mYD literal 2173 zcmYdHPt{G$OD?J`D9Oyv)5|YP*Do{V(zR14F3!+RO))YxHMCTS$_Oh?O;-plcF|5Y zD)lgSa?Nl|4fiSVEe%LbO%E%~a`Un*PRtDO(f73U%rZ+hOy}||%*`~(Gb>CtPc5=E z$~P`ea`jEC3WzK%cFK%OOv`gL$_e+;Hjni8H9)s5xX2^1&`}|>Dm=@z!q_ClC^z3A zG(6BSGP}gcrO-Xk-!<6HCos*e$S}h*AR^b%*@P>o)IB&n(5bL2Hziy@*wxIW#5dc_ z&m}X^FDE@K)uPJK%`-SVH!r!Q7{j(4_rkEKbcM*`kTMICB9Gjt0LMHJ*Zi=|WVf=k zvM{Hp5Q}hk{ldzez=)(^ZzuPlWUj(g6LWooRLgP$GyPImHxIw?2(L5?{j3~6j|dNg zqD=2_mvH@11ILV9Uv$4Y2m4lLq$}hnMy014B!?P>g}a*Pn&lQ&W(F1}m0Cu62Zv=^ zmgQHJTNWiH29+lUT5$P$lsFlf8N}gmUTX>MEE-n5Vi{7CX9Fx;sWxlo+~)I~r8xg_j0cl$xbDoA{bUc(|MS7zcV< z7;xPS@@!XQHts+1fb-{^eMMF5>}|$} zrVIUAX7GH&(h2{=sxK|EQTlT)bK|{7uTIu2I3ZeFHn~S=>DS^KK8?9gj$W635?Frs zt!adYl|l8Dl&$XCx3_Pp<@8pFk=`_C(}`_=_G`|l#2 zbH2AY&&q682EUljf#W}z_S{L(3OXjdzaZ$>R)+0PA%U-JFQ(gCicfC#OG9qA#kWn-Ib5?Mqqs4Teo#z?7A&}oWJ&u z=rzIeH|t+sGQ49QTbT8xv;OS+q*)VUqP1p8EvPyDXZx?p@)NyLJs0em9o|GfI@7dN zR6cv*)y^GK%Ur8A`(9o1L3-Ev`?ZAyn_h_h?GRyK>UQT#`A#AK1uEU*|AZ~G4dUv8 znG&C>_Q&XbbMm{xzbY*9V%^l*AjxS5!Z{iIk2?7=`sn-Ysa@_M$QhvJT$maE=B3D$ zIOF0_6$jrR-uv|}g}-jNa>9YVA!o6cKljpT9tTB%qpyt4?K{psdF$5+nKGpuI~QKvMJvBYxS&KXBh8%>I>$mc3jik{VMnEe^#z%5)bYub0+2s z^&da{XKv58qg?UN-e0cr6PYM?r7N{_vE!Wu`hlifrYt&QkP+S|$Dp25QMWbD&d< z)!YN_Z=Sb)YUfRJW8(I+4m9n2y|=TAeVw?2ih#rVb4vrgE14r~Js&g$ew&wnK%BQT zb^Z0AFBR3|zI)57Y&o<3{*+m8bw=rp0;|aCfUNzCvX^ggtr9-hAG0aw*W{A&%%zGm zX6ei9_;SGi)sx-2r3oia-k%#{SDSmbbxJ~w+7q@H^BgQ^{)=lmwfJVqzS>{?71H6` zoVJKneCl0QTiw0Kxh&;MgT(KE`&(Tb1tsq6Wh$M)#MhkgXT`guCd&;R^K3h9uI65H zPg{RzpXfY&mybfr?#3+Y=@fFzU%}T^FLyFUT19%=m*xI!59+rn?4Nh+8P}}+%}0;F zIk4H9XO;i5dtX)Gy5(=HJkR1YEp^q5k0rWQf<=4YvjrKtTus^NogT_?XSen92M-+f z2&R5pxbNtPM{BJ=3eDC2|8)1!=zHaSrH}kxJQTbCuQgJBairVq`)Mn7E6cbDE);HB zwq?fW1tkSS`@EbVatqB<5!3p$V2jY#MXkGkJX^@MXnLGX3Q$c8t%Qe zoh|t9;K}T)9Hq!DQ3~=-ueLuCt7~6Vs>^U2ej`Z}Ifvr=pT4 za~y3OKEGsaUKF$1=4iatzIuVDD(kwRmQGl+Jpb~?wN^qK^J278!ZE tX`P~L#MG^w#px5`zWm556mz_}hyT{&BWeF6=1+eXFrn%mW7W&X9sqIz=g9y7