diff --git a/.github/workflows/build-ami.yml b/.github/workflows/build-ami.yml index 0cb33b9..77d7d18 100644 --- a/.github/workflows/build-ami.yml +++ b/.github/workflows/build-ami.yml @@ -185,8 +185,8 @@ jobs: echo "2. Attach an IAM instance profile with S3 read/write access to \`${{ vars.BACKUP_BUCKET }}\`" echo "3. SSH in and restore data:" echo " \`\`\`" - echo " sudo BACKUP_BUCKET=${{ vars.BACKUP_BUCKET }} bash /opt/mediawiki-ami/restore/restore.sh" - echo " sudo bash /opt/mediawiki-ami/restore/upgrade-1.35-to-1.43.sh" + echo " sudo BACKUP_BUCKET=${{ vars.BACKUP_BUCKET }} bash /opt/mediawiki-ami/restore.sh" + echo " sudo bash /opt/mediawiki-ami/upgrade-1.35-to-1.43.sh" echo " \`\`\`" echo "4. Validate, then move the Elastic IP to cut over" } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index 7c20d47..a35a727 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,11 @@ You can also trigger it manually from the Actions UI with an optional dry-run. ## Step 4 — Launch the new instance 1. Launch an EC2 instance from the new AMI (t4g.medium, us-east-2, same VPC as old server) -2. Attach an IAM instance profile with `s3:GetObject` + `s3:ListBucket` on `$BACKUP_BUCKET` +2. Attach an IAM instance profile with `s3:GetObject` + `s3:ListBucket` on `$BACKUP_BUCKET`, + plus the AWS-managed `CloudWatchAgentServerPolicy` (see + [docs/s3-backup-setup.md](docs/s3-backup-setup.md#step-5--iam-policy-for-the-ec2-instance-profile)) — + the CloudWatch agent is already installed, configured, and enabled at boot by the AMI; + it just needs permission to publish. 3. Attach the same Elastic IP / security groups as the old server (but do **not** move the EIP yet) --- @@ -128,6 +132,10 @@ cat /tmp/mw-update-*.txt # Verify backup cron is installed sudo cat /etc/cron.d/mediawiki-backup sudo cat /etc/sysconfig/mediawiki-backup # confirm BACKUP_BUCKET is set + +# Verify CloudWatch agent is running and publishing (no AccessDenied errors) +sudo systemctl status amazon-cloudwatch-agent +sudo tail -50 /var/log/amazon-cloudwatch-agent.log ``` When satisfied: @@ -170,7 +178,7 @@ via `/etc/cron.d/mediawiki-backup`. It uploads to S3 with GFS rotation: ``` config/ - cloudwatch/mediawiki-cwa.json ← CloudWatch agent config + cloudwatch/mediawiki-cwa.json ← CloudWatch agent config (metrics + logs, installed & enabled at boot) cron/mediawiki-backup ← Cron job definitions cron/mediawiki-jobs ← MediaWiki job runner cron httpd/mediawiki.conf ← Apache vhost template @@ -254,3 +262,73 @@ role that eliminates the need for long-lived access keys entirely. | `MW_DISCOURSE_SECRET` | Discourse SSO shared secret ⚠ rotate | | `BACKUP_BUCKET` | S3 bucket name for backups | +--- + +## MediaWiki extensions + +`packer/scripts/05-extensions.sh` installs extensions via Composer, following +MediaWiki best practices +([Composer/For_extensions](https://www.mediawiki.org/wiki/Composer/For_extensions), +[Composer.json_best_practices](https://www.mediawiki.org/wiki/Manual:Composer.json_best_practices)). + +**Key principle:** never modify MediaWiki core's `composer.json` directly. +Extensions are declared in `config/mediawiki/composer.local.json`, which is +merged automatically by the `wikimedia/composer-merge-plugin` already +configured in core's `composer.json`. + +**Extension categories:** + +- **Bundled in MW 1.43** (no installation needed — just `wfLoadExtension`): + AbuseFilter, CategoryTree, Cite, CiteThisPage, CodeEditor, ConfirmEdit, + DiscussionTools, Echo, Gadgets, ImageMap, InputBox, Interwiki, Linter, + LoginNotify, Math, MultimediaViewer, Nuke, OATHAuth, PageImages, + ParserFunctions, PdfHandler, Poem, README, ReplaceText, Scribunto, + SecureLinkFixer, SpamBlacklist, SyntaxHighlight_GeSHi, TemplateData, + TextExtracts, Thanks, TitleBlacklist, VisualEditor, WikiEditor +- **Installed via Composer** (`composer.local.json`): DiscourseSsoConsumer + (→ PluggableAuth), IFrameTag, TemplateStyles, JsonConfig, PluggableAuth, + WikiCategoryTagCloud + + > TemplateStyles, JsonConfig, and WikiCategoryTagCloud are declared as + > `"package"` repositories (not `"vcs"`) because their upstream + > `composer.json` files lack a `"name"` field. Composer's `vcs` driver + > requires a name to resolve the package and skips branches without one + > (`"Unknown package has no name defined"`). The `"package"` type supplies + > the metadata inline, bypassing that requirement. + +**How to add or update an extension:** + +1. Add a VCS repository entry in `config/mediawiki/composer.local.json`. +2. Add the package `require` line with the correct version constraint. +3. For Gerrit extensions, use `dev-REL1_XX` matching the MW branch. +4. For tagged releases, use the semver tag (e.g. `5.0.2`). +5. Run `packer/scripts/05-extensions.sh` (or `composer update --no-dev` in + the MW root) to verify resolution. +6. Add the corresponding `wfLoadExtension()`/config to + `config/mediawiki/LocalSettings.php` — the **only** place extension + loading and configuration is defined. `05-extensions.sh` only installs + code into `extensions/`; it never writes to `LocalSettings.php`. + +--- + +## Contributing + +Issues and pull requests are welcome. + +- **Adding/updating a MediaWiki extension** — see + [MediaWiki extensions](#mediawiki-extensions) above. +- **Changing build scripts** (`packer/scripts/*.sh`) — validate locally with + `packer-test/test-local.sh` before opening a PR; it runs the scripts in a + container against `mock-aws` without needing real AWS credentials. +- **Changing the Packer template** — run + `packer validate -var-file=my.auto.pkrvars.hcl packer/` and, where + feasible, a full `packer build` against a scratch VPC. +- **Changing backup/restore scripts** (`scripts/backup/`, `scripts/restore/`) + — test against a disposable bucket/instance; these scripts touch + production data paths. +- Keep secrets out of commits and PR descriptions; use the + `PKR_VAR_*`/GitHub Secrets mechanisms documented above. +- Open a PR against `main`; CI runs `packer-validate.yml` on every PR and + `build-ami.yml` on tag pushes. + + diff --git a/config/cloudwatch/mediawiki-cwa.json b/config/cloudwatch/mediawiki-cwa.json index 2318b78..aa9e111 100644 --- a/config/cloudwatch/mediawiki-cwa.json +++ b/config/cloudwatch/mediawiki-cwa.json @@ -1,4 +1,8 @@ { + "agent": { + "metrics_collection_interval": 60, + "logfile": "/var/log/amazon-cloudwatch-agent.log" + }, "logs": { "logs_collected": { "files": { @@ -7,33 +11,75 @@ "file_path": "/var/log/httpd/mediawiki-error.log", "log_group_name": "/mediawiki/apache/error", "log_stream_name": "{instance_id}", - "timezone": "UTC" + "retention_in_days": 7, + "auto_removal": true }, { - "file_path": "/var/log/httpd/mediawiki-access.log", - "log_group_name": "/mediawiki/apache/access", + "file_path": "/var/log/php_errors.log", + "log_group_name": "/mediawiki/php/error", "log_stream_name": "{instance_id}", - "timezone": "UTC" + "retention_in_days": 7, + "auto_removal": true }, { "file_path": "/var/log/mariadb/slow.log", "log_group_name": "/mediawiki/mariadb/slow", "log_stream_name": "{instance_id}", - "timezone": "UTC" + "retention_in_days": 7, + "auto_removal": true }, { "file_path": "/var/log/mediawiki-backup.log", "log_group_name": "/mediawiki/backup", "log_stream_name": "{instance_id}", - "timezone": "UTC" + "retention_in_days": 7, + "auto_removal": true } ] } + } + }, + "metrics": { + "namespace": "MediaWiki", + "append_dimensions": { + "InstanceId": "${aws:InstanceId}", + "InstanceType": "${aws:InstanceType}" }, + "aggregation_dimensions": [["InstanceId"]], "metrics_collected": { - "cpu": { "measurement": ["usage_active"], "metrics_collection_interval": 60 }, - "disk": { "measurement": ["used_percent"], "metrics_collection_interval": 60 }, - "mem": { "measurement": ["mem_used_percent"], "metrics_collection_interval": 60 } + "cpu": { + "measurement": ["usage_active", "usage_iowait", "usage_user", "usage_system"], + "metrics_collection_interval": 60, + "totalcpu": true + }, + "mem": { + "measurement": ["mem_used_percent", "mem_available_percent"], + "metrics_collection_interval": 60 + }, + "swap": { + "measurement": ["swap_used_percent"], + "metrics_collection_interval": 60 + }, + "disk": { + "measurement": ["used_percent", "inodes_free"], + "metrics_collection_interval": 60, + "resources": ["/"], + "drop_device": true + }, + "diskio": { + "measurement": ["io_time", "read_bytes", "write_bytes"], + "metrics_collection_interval": 60, + "resources": ["*"] + }, + "netstat": { + "measurement": ["tcp_established", "tcp_time_wait"], + "metrics_collection_interval": 60 + }, + "procstat": [ + { "pattern": "httpd", "measurement": ["cpu_usage", "memory_rss", "num_fds"] }, + { "pattern": "php-fpm", "measurement": ["cpu_usage", "memory_rss", "num_fds"] }, + { "pattern": "mariadbd", "measurement": ["cpu_usage", "memory_rss", "num_fds"] } + ] } } } diff --git a/config/cron/mediawiki-backup b/config/cron/mediawiki-backup index 204a621..e7de200 100644 --- a/config/cron/mediawiki-backup +++ b/config/cron/mediawiki-backup @@ -4,5 +4,5 @@ # Edit /etc/sysconfig/mediawiki-backup to configure bucket and retention SHELL=/bin/bash -0 8 * * * root /opt/mediawiki-ami/backup/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1 +0 8 * * * root /opt/mediawiki-ami/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1 diff --git a/config/httpd/mediawiki.conf b/config/httpd/mediawiki.conf index cd0c478..0a4771b 100644 --- a/config/httpd/mediawiki.conf +++ b/config/httpd/mediawiki.conf @@ -2,7 +2,7 @@ # # HTTP redirects to HTTPS immediately. The HTTPS vhost uses the self-signed # cert created by mod_ssl on first install; certbot replaces it post-launch: -# sudo bash /opt/mediawiki-ami/setup/setup-ssl.sh +# sudo bash /opt/mediawiki-ami/setup-ssl.sh # ── HTTP: redirect to HTTPS ─────────────────────────────────────────────────── diff --git a/config/mediawiki/LocalSettings.php b/config/mediawiki/LocalSettings.php index 633b22e..44b0aaf 100644 --- a/config/mediawiki/LocalSettings.php +++ b/config/mediawiki/LocalSettings.php @@ -106,6 +106,7 @@ $wgUploadPath = "$wgScriptPath/images"; $wgUploadDirectory = "$IP/images"; $wgMaxUploadSize = 104857600; # 100 MiB +$wgMaxImageArea = 6.4e7; # Same additional types as 1.35 server $wgFileExtensions = array_merge( @@ -163,6 +164,7 @@ wfLoadSkin( 'Vector' ); wfLoadSkin( 'Timeless' ); wfLoadSkin( 'MonoBook' ); +wfLoadSkin( 'MinervaNeue' ); $wgDefaultSkin = "timeless"; # ============================================================================= @@ -192,15 +194,21 @@ # Bundled extensions (shipped with MW 1.43 tarball) # ============================================================================= +wfLoadExtension( 'AbuseFilter' ); wfLoadExtension( 'CategoryTree' ); wfLoadExtension( 'Cite' ); wfLoadExtension( 'CiteThisPage' ); wfLoadExtension( 'CodeEditor' ); wfLoadExtension( 'ConfirmEdit' ); +wfLoadExtension( 'DiscussionTools' ); +wfLoadExtension( 'Echo' ); wfLoadExtension( 'Gadgets' ); wfLoadExtension( 'ImageMap' ); wfLoadExtension( 'InputBox' ); wfLoadExtension( 'Interwiki' ); +wfLoadExtension( 'Linter' ); +wfLoadExtension( 'LoginNotify' ); +wfLoadExtension( 'Math' ); wfLoadExtension( 'MultimediaViewer' ); wfLoadExtension( 'Nuke' ); wfLoadExtension( 'OATHAuth' ); @@ -214,18 +222,34 @@ wfLoadExtension( 'SyntaxHighlight_GeSHi' ); wfLoadExtension( 'TemplateData' ); wfLoadExtension( 'TextExtracts' ); +wfLoadExtension( 'Thanks' ); wfLoadExtension( 'TitleBlacklist' ); wfLoadExtension( 'VisualEditor' ); wfLoadExtension( 'WikiEditor' ); -# ============================================================================= -# Separately installed extensions (Gerrit REL1_43) -# ============================================================================= +# AbuseFilter — rule-based anti-spam/vandalism filters +$wgAbuseFilterActions = [ + 'throttle' => true, + 'warn' => true, + 'disallow' => true, + 'blockautopromote' => true, + 'block' => true, + 'tag' => true, +]; + +# LoginNotify — notifies users of logins from new devices/locations +$wgLoginNotifyUseEcho = true; + +# Math — LaTeX formula rendering (useful for electronics/physics pages) +$wgDefaultUserOptions['math'] = 'mathml'; # Lua scripting — required by Infobox, Navbox, and excerpt templates wfLoadExtension( 'Scribunto' ); $wgScribuntoDefaultEngine = 'luastandalone'; -$wgScribuntoEngineConf['luastandalone']['luaPath'] = '/usr/bin/lua'; +# NOTE: no luaPath override. Neither Scribunto's bundled binaries, LuaBinaries, +# nor LuaJIT (unsupported — phab:T184156) are available for arm64/Graviton. +# 04-mediawiki.sh compiles Lua 5.1.5 from source and installs it at +# Scribunto's own default bundled-binary path, so auto-detection just works. $wgScribuntoUseCodeEditor = true; # Per-template CSS — used by common Scribunto modules diff --git a/docs/s3-backup-setup.md b/docs/s3-backup-setup.md index 59c8024..4f54ca2 100644 --- a/docs/s3-backup-setup.md +++ b/docs/s3-backup-setup.md @@ -90,8 +90,18 @@ aws s3api get-bucket-lifecycle-configuration --bucket "${BACKUP_BUCKET}" ## Step 5 — IAM policy for the EC2 instance profile -The instance needs permission to read and write within the backup prefix. -Attach the following inline policy to the instance's IAM role: +The instance needs permission to read and write within the backup prefix, and +to publish metrics/logs to CloudWatch (the agent is installed and enabled by +`06-finalize.sh`, but will silently fail to publish without this permission). + +Attach the AWS-managed **`CloudWatchAgentServerPolicy`** plus the following +inline policy to the instance's IAM role: + +```bash +aws iam attach-role-policy \ + --role-name \ + --policy-arn arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy +``` ```json { @@ -114,6 +124,18 @@ Attach the following inline policy to the instance's IAM role: } ``` +### Verify the agent is running and publishing + +```bash +sudo systemctl status amazon-cloudwatch-agent +sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl -a status +sudo tail -50 /var/log/amazon-cloudwatch-agent.log +``` + +If the role is missing `CloudWatchAgentServerPolicy`, the log shows +`AccessDenied`/`UnauthorizedOperation` errors even though the service is +`active (running)`. + --- ## Step 6 — Configure the instance @@ -153,7 +175,7 @@ aws s3 cp "s3://${BACKUP_BUCKET}/backups/daily/manifest.txt" - --region "${AWS_R ```bash sudo BACKUP_BUCKET=my-mediawiki-backups \ - bash /opt/mediawiki-ami/backup/backup-with-retention.sh + bash /opt/mediawiki-ami/backup-with-retention.sh ``` ### Verify lifecycle rules are expiring objects @@ -177,17 +199,17 @@ For the initial migration from 1.35, also run `scripts/restore/upgrade-1.35-to-1 ```bash # Restore from latest daily backup sudo BACKUP_BUCKET=my-mediawiki-backups \ - bash /opt/mediawiki-ami/restore/restore.sh + bash /opt/mediawiki-ami/restore.sh # Restore from a specific weekly backup sudo BACKUP_BUCKET=my-mediawiki-backups \ BACKUP_TIMESTAMP=weekly/2026-W24 \ - bash /opt/mediawiki-ami/restore/restore.sh + bash /opt/mediawiki-ami/restore.sh # Restore from a specific monthly backup sudo BACKUP_BUCKET=my-mediawiki-backups \ BACKUP_TIMESTAMP=monthly/2026-05 \ - bash /opt/mediawiki-ami/restore/restore.sh + bash /opt/mediawiki-ami/restore.sh ``` --- diff --git a/packer-test/test-local.sh b/packer-test/test-local.sh index 6117ca2..219d587 100755 --- a/packer-test/test-local.sh +++ b/packer-test/test-local.sh @@ -304,7 +304,7 @@ make-backup) podman exec -it "${CONTAINER_NAME}" \ bash -c 'mkdir -p /tmp/mock-s3 && export MOCK_S3_ROOT=/tmp/mock-s3 BACKUP_BUCKET=mock-bucket AWS_REGION=us-east-2 && - bash /opt/mediawiki-ami/backup/full-backup.sh' + bash /opt/mediawiki-ami/full-backup.sh' echo "==> Copying backup out of container → ${LOCAL_BACKUP_DIR}…" # Clear previous test backup so old artefacts don't confuse restore tests @@ -376,7 +376,7 @@ test-restore) TIMESTAMP_EXPORT="" [[ -n "${BACKUP_TAG}" ]] && TIMESTAMP_EXPORT="export BACKUP_TIMESTAMP=${BACKUP_TAG} &&" podman exec -it "${CONTAINER_NAME}" \ - bash -c "export MOCK_S3_ROOT=/tmp/mock-s3 BACKUP_BUCKET=mock-bucket AWS_REGION=us-east-2 && ${TIMESTAMP_EXPORT} bash /opt/mediawiki-ami/restore/restore.sh" + bash -c "export MOCK_S3_ROOT=/tmp/mock-s3 BACKUP_BUCKET=mock-bucket AWS_REGION=us-east-2 && ${TIMESTAMP_EXPORT} bash /opt/mediawiki-ami/restore.sh" ;; # ── test-upgrade ────────────────────────────────────────────────────────────── @@ -386,7 +386,7 @@ test-upgrade) require_container echo "==> Running upgrade-1.35-to-1.43.sh inside '${CONTAINER_NAME}'…" podman exec -it "${CONTAINER_NAME}" \ - bash /opt/mediawiki-ami/restore/upgrade-1.35-to-1.43.sh + bash /opt/mediawiki-ami/upgrade-1.35-to-1.43.sh ;; *) diff --git a/packer/mediawiki.pkr.hcl b/packer/mediawiki.pkr.hcl index 63f89e6..eb8b784 100644 --- a/packer/mediawiki.pkr.hcl +++ b/packer/mediawiki.pkr.hcl @@ -182,8 +182,9 @@ build { "php --version", "mysqladmin --version", "httpd -v", + "/var/www/mediawiki/extensions/Scribunto/includes/Engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua -v", "php /var/www/mediawiki/maintenance/checkDependencies.php || true", - "systemctl is-enabled httpd mariadb crond", + "systemctl is-enabled httpd php-fpm mariadb crond amazon-cloudwatch-agent", ] execute_command = "sudo bash '{{ .Path }}'" } diff --git a/packer/scripts/00-system.sh b/packer/scripts/00-system.sh index 4255b46..a0245bb 100644 --- a/packer/scripts/00-system.sh +++ b/packer/scripts/00-system.sh @@ -24,10 +24,26 @@ dnf install -y \ nfs-utils \ python3 \ python3-pip \ - lua \ diffutils \ cronie \ - gettext + gettext \ + gcc \ + make + +# NOTE: no distro Lua package is installed here. Per +# https://www.mediawiki.org/wiki/Extension:Scribunto#Lua_binary : +# - Scribunto's bundled LuaStandalone binaries are x86/x86-64/Mac/Windows +# only — no arm64 build exists. +# - LuaBinaries (http://luabinaries.sourceforge.net/) — the docs' own +# suggested source for additional binaries — likewise ships Linux +# binaries for x86/x86_64 only, no aarch64. +# - LuaJIT is explicitly "not supported" (removed for Spectre/bitrot +# concerns, phab:T184156) even though API/ABI-compatible. +# - AL2023 has no lua5.1 package (only lua 5.4). +# The only correct option on Graviton is to compile Lua 5.1.5 from source, +# which 04-mediawiki.sh does, installing it at Scribunto's own default +# bundled binary path so no $wgScribuntoEngineConf luaPath override is +# needed. # ── Enable crond for scheduled backups ──────────────────────────────────────── systemctl enable --now crond diff --git a/packer/scripts/04-mediawiki.sh b/packer/scripts/04-mediawiki.sh index 607e78d..23fea7f 100644 --- a/packer/scripts/04-mediawiki.sh +++ b/packer/scripts/04-mediawiki.sh @@ -57,6 +57,41 @@ mv "/var/www/mediawiki-${MW_VERSION}" "${MW_ROOT}" mkdir -p "${MW_ROOT}/images" mkdir -p "${MW_ROOT}/maintenance" +# ── Compile Lua 5.1.5 for Scribunto (arm64) ────────────────────────────────── +# Neither Scribunto's bundled binaries nor LuaBinaries (the docs' suggested +# fallback: https://www.mediawiki.org/wiki/Extension:Scribunto#Additional_binaries) +# ship an aarch64 build — both are x86/x86_64-only — and LuaJIT is explicitly +# unsupported by Scribunto (Spectre/bitrot concerns, phab:T184156). AL2023 +# also has no lua5.1 package. Compiling from source is the documented +# fallback ("or from your Linux distribution") when no binary is available. +# +# This script runs natively on the arm64 Packer builder, so a plain 'make +# generic' build (the same target used for Scribunto's own bundled x86 +# binaries) produces a correct native binary with no cross-compilation. +# Installing it at Scribunto's own default bundled path means no +# $wgScribuntoEngineConf luaPath override is required — matches the "should +# work out of the box" behavior the docs describe for supported platforms. +LUA_BUILD_DIR="/tmp/lua-5.1.5" +SCRIBUNTO_LUA_BIN="${MW_ROOT}/extensions/Scribunto/includes/Engines/LuaStandalone/binaries/lua5_1_5_linux_64_generic/lua" + +[ -d "$(dirname "${SCRIBUNTO_LUA_BIN}")" ] || { + echo "ERROR: Scribunto LuaStandalone binaries dir not found — is Scribunto bundled in this MW tarball?" + exit 1 +} + +echo "Compiling Lua 5.1.5 (generic) natively for arm64…" +cd /tmp +curl -fsSL -o lua-5.1.5.tar.gz https://www.lua.org/ftp/lua-5.1.5.tar.gz +rm -rf "${LUA_BUILD_DIR}" +tar -xzf lua-5.1.5.tar.gz -C /tmp +make -C "${LUA_BUILD_DIR}" generic + +install -m 755 -o root -g root "${LUA_BUILD_DIR}/src/lua" "${SCRIBUNTO_LUA_BIN}" +"${SCRIBUNTO_LUA_BIN}" -v + +rm -rf "${LUA_BUILD_DIR}" /tmp/lua-5.1.5.tar.gz +echo "Lua 5.1.5 (arm64) installed at ${SCRIBUNTO_LUA_BIN}" + # ── Directory permissions ───────────────────────────────────────────────────── chown -R apache:apache "${MW_ROOT}" # images/ must be writable by Apache diff --git a/packer/scripts/05-extensions.sh b/packer/scripts/05-extensions.sh index 70c23e9..674308e 100644 --- a/packer/scripts/05-extensions.sh +++ b/packer/scripts/05-extensions.sh @@ -1,45 +1,10 @@ #!/usr/bin/env bash # packer/scripts/05-extensions.sh -# Phase 5: Install MediaWiki extensions compatible with 1.43 (REL1_43 branch). +# Phase 5: Install MediaWiki extensions compatible with 1.43 (REL1_43 branch) +# via Composer, using config/mediawiki/composer.local.json. # -# INSTALLATION STRATEGY: -# This script uses Composer (the dependency manager bundled with MediaWiki's -# ecosystem) to install extensions, following MediaWiki best practices: -# - https://www.mediawiki.org/wiki/Composer/For_extensions -# - https://www.mediawiki.org/wiki/Manual:Composer.json_best_practices -# -# Key principle: NEVER modify MediaWiki's composer.json directly. -# Instead, extensions are declared in composer.local.json which is merged -# automatically by the wikimedia/composer-merge-plugin already configured -# in MediaWiki core's composer.json. -# -# EXTENSION CATEGORIES: -# -# Bundled in MW 1.43 (no installation needed — just wfLoadExtension): -# AbuseFilter CategoryTree Cite CiteThisPage CodeEditor ConfirmEdit -# DiscussionTools Echo Gadgets ImageMap InputBox Interwiki Linter -# LoginNotify Math MultimediaViewer Nuke OATHAuth PageImages -# ParserFunctions PdfHandler Poem README ReplaceText Scribunto -# SecureLinkFixer SpamBlacklist SyntaxHighlight_GeSHi TemplateData -# TextExtracts Thanks TitleBlacklist VisualEditor WikiEditor -# -# Installed via Composer (composer.local.json): -# DiscourseSsoConsumer (-> PluggableAuth), IFrameTag -# TemplateStyles, JsonConfig, PluggableAuth, WikiCategoryTagCloud -# -# NOTE: TemplateStyles, JsonConfig, and WikiCategoryTagCloud are declared as -# "package" repositories (not "vcs") in composer.local.json because their -# upstream composer.json files lack a "name" field. Composer's "vcs" driver -# requires a name to resolve the package and skips branches without one -# ("Unknown package has no name defined"). The "package" type lets us supply -# the metadata inline, bypassing that requirement entirely. -# -# HOW TO ADD/UPDATE AN EXTENSION: -# 1. Add a VCS repository entry in config/mediawiki/composer.local.json -# 2. Add the package "require" line with the correct version constraint -# 3. For Gerrit extensions: use "dev-REL1_XX" matching the MW branch -# 4. For tagged releases: use the semver tag (e.g. "5.0.2") -# 5. Run this script (or `composer update --no-dev` in the MW root) +# See README.md § "MediaWiki extensions" for installation strategy, +# extension categories, and how to add/update an extension. set -euxo pipefail @@ -141,6 +106,7 @@ echo "Composer update completed successfully" # DB_MASTER was renamed to DB_PRIMARY in MW 1.37 and removed in 1.42. # The 6.x release that fixed this requires PHP 8.4, which MW 1.43 does not # support. Patch the installed source directly; DB_PRIMARY has the same value. +# TODO: [16-Jul-2026 04:01:55 UTC] PHP Deprecated: Use of wfGetDB was deprecated in MediaWiki 1.39. [Called from MediaWiki\Extension\DiscourseSsoConsumer\Db::ensureCurrentSchema in /var/www/mediawiki/extensions/DiscourseSsoConsumer/src/Db.php at line 83] in /var/www/mediawiki/includes/debug/MWDebug.php on line 385 echo "Patching DiscourseSsoConsumer: DB_MASTER → DB_PRIMARY" grep -rl --include='*.php' 'DB_MASTER' "${EXT_DIR}/DiscourseSsoConsumer" \ | xargs sed -i 's/\bDB_MASTER\b/DB_PRIMARY/g' @@ -156,123 +122,7 @@ done # ── Fix ownership ───────────────────────────────────────────────────────────── chown -R apache:apache "${EXT_DIR}" -# ── Append extension loading to LocalSettings.php ───────────────────────────── -LSETTINGS="${MW_ROOT}/LocalSettings.php" -{ - echo "" - echo "# === Bundled extensions (shipped with MW 1.43 tarball) ===" - echo "wfLoadExtension( 'AbuseFilter' );" - echo "wfLoadExtension( 'CategoryTree' );" - echo "wfLoadExtension( 'Cite' );" - echo "wfLoadExtension( 'CiteThisPage' );" - echo "wfLoadExtension( 'CodeEditor' );" - echo "wfLoadExtension( 'ConfirmEdit' );" - echo "wfLoadExtension( 'DiscussionTools' );" - echo "wfLoadExtension( 'Echo' );" - echo "wfLoadExtension( 'Gadgets' );" - echo "wfLoadExtension( 'ImageMap' );" - echo "wfLoadExtension( 'InputBox' );" - echo "wfLoadExtension( 'Interwiki' );" - echo "wfLoadExtension( 'Linter' );" - echo "wfLoadExtension( 'LoginNotify' );" - echo "wfLoadExtension( 'Math' );" - echo "wfLoadExtension( 'MultimediaViewer' );" - echo "wfLoadExtension( 'Nuke' );" - echo "wfLoadExtension( 'OATHAuth' );" - echo "wfLoadExtension( 'PageImages' );" - echo "wfLoadExtension( 'ParserFunctions' );" - echo "wfLoadExtension( 'PdfHandler' );" - echo "wfLoadExtension( 'Poem' );" - echo "wfLoadExtension( 'ReplaceText' );" - echo "wfLoadExtension( 'SecureLinkFixer' );" - echo "wfLoadExtension( 'SpamBlacklist' );" - echo "wfLoadExtension( 'SyntaxHighlight_GeSHi' );" - echo "wfLoadExtension( 'TemplateData' );" - echo "wfLoadExtension( 'TextExtracts' );" - echo "wfLoadExtension( 'Thanks' );" - echo "wfLoadExtension( 'TitleBlacklist' );" - echo "wfLoadExtension( 'VisualEditor' );" - echo "wfLoadExtension( 'WikiEditor' );" - echo "" - echo "# AbuseFilter — rule-based anti-spam/vandalism filters" - echo "\$wgAbuseFilterActions = ['throttle' => true, 'warn' => true, 'disallow' => true," - echo " 'blockautopromote' => true, 'block' => true, 'tag' => true];" - echo "" - echo "# LoginNotify — notifies users of logins from new devices/locations" - echo "\$wgLoginNotifyUseEcho = true;" - echo "" - echo "# Math — LaTeX formula rendering (useful for electronics/physics pages)" - echo "\$wgDefaultUserOptions['math'] = 'mathml';" - echo "" - echo "# === Extensions installed via Composer (composer.local.json) ===" - for item in "${EXPECTED_EXTENSIONS[@]}"; do - echo "wfLoadExtension( '${item}' );" - done - echo "" - echo "# === Skins (all four ship with the MW 1.43 tarball) ===" - echo "# Vector is the default and loaded automatically." - echo "wfLoadSkin( 'MonoBook' );" - echo "wfLoadSkin( 'Timeless' );" - echo "wfLoadSkin( 'MinervaNeue' );" - echo "" - echo "# === Extension configuration ===" - echo "" - echo "# Scribunto (Lua)" - echo "\$wgScribuntoDefaultEngine = 'luastandalone';" - echo "\$wgScribuntoEngineConf['luastandalone']['luaPath'] = '/usr/bin/lua';" - echo "\$wgScribuntoUseCodeEditor = true;" - echo "" - echo "# JsonConfig — data pages from Commons" - echo "\$wgJsonConfigEnableLuaSupport = true;" - echo "\$wgJsonConfigModels['Tabular.JsonConfig'] = 'JsonConfig\\\\JCTabularContent';" - echo "\$wgJsonConfigs['Tabular.JsonConfig'] = [" - echo " 'namespace' => 486," - echo " 'nsName' => 'Data'," - echo " 'pattern' => '/.\.tab$/'," - echo " 'license' => 'CC0-1.0'," - echo " 'isLocal' => false," - echo "];" - echo "\$wgJsonConfigModels['Map.JsonConfig'] = 'JsonConfig\\\\JCMapDataContent';" - echo "\$wgJsonConfigs['Map.JsonConfig'] = [" - echo " 'namespace' => 486," - echo " 'nsName' => 'Data'," - echo " 'pattern' => '/.\.map$/'," - echo " 'license' => 'CC0-1.0'," - echo " 'isLocal' => false," - echo "];" - echo "\$wgJsonConfigInterwikiPrefix = 'commons';" - echo "\$wgJsonConfigs['Tabular.JsonConfig']['remote'] = [" - echo " 'url' => 'https://commons.wikimedia.org/w/api.php'" - echo "];" - echo "\$wgJsonConfigs['Map.JsonConfig']['remote'] = [" - echo " 'url' => 'https://commons.wikimedia.org/w/api.php'" - echo "];" - echo "" - echo "# TitleBlacklist" - echo "\$wgGroupPermissions['sysop']['tboverride'] = false;" - echo "\$wgTitleBlacklistSources = [" - echo " [" - echo " 'type' => 'localpage'," - echo " 'src' => 'MediaWiki:Titleblacklist'" - echo " ]" - echo "];" - echo "" - echo "# WikiCategoryTagCloud" - echo "# (no additional config needed — just loads)" - echo "" - echo "# IFrameTag" - echo "\$iFrameOnWikiConfig = false;" - echo "\$iFrameDomains = [" - echo " 'restreamer.asmbly.org'" - echo "];" - echo "" - echo "# PluggableAuth + DiscourseSsoConsumer" - echo "# NOTE: PluggableAuth v7.x for MW 1.39+ uses a different config format." - echo "# The restore script will merge the old Discourse SSO settings." - echo "# Uncomment and configure after verifying DiscourseSsoConsumer compatibility:" - echo "# \$wgPluggableAuth_EnableLocalLogin = false;" - echo "# \$wgPluggableAuth_ButtonLabelMessage = 'Log In With Discourse';" -} >> "${LSETTINGS}" - +# Extension loading/config lives solely in config/mediawiki/LocalSettings.php +# (deployed by 04-mediawiki.sh); this script only installs code into extensions/. echo "05-extensions.sh complete" diff --git a/packer/scripts/06-finalize.sh b/packer/scripts/06-finalize.sh index ac4cc28..85ebf04 100644 --- a/packer/scripts/06-finalize.sh +++ b/packer/scripts/06-finalize.sh @@ -11,8 +11,9 @@ for svc in postfix; do systemctl disable "${svc}" 2>/dev/null && systemctl stop "${svc}" 2>/dev/null || true done -# Ensure only httpd and mariadb are enabled at boot -systemctl enable httpd mariadb crond +# Ensure httpd, php-fpm, mariadb, and crond are enabled at boot +# (amazon-cloudwatch-agent is enabled separately, below, once its config is staged) +systemctl enable httpd php-fpm mariadb crond # ── MediaWiki file permissions (final pass) ─────────────────────────────────── # Core files: root-owned, apache-readable @@ -37,13 +38,21 @@ chown root:apache "${MW_ROOT}/LocalSettings.php" cp /tmp/config/cron/mediawiki-jobs /etc/cron.d/mediawiki-jobs chmod 644 /etc/cron.d/mediawiki-jobs -# ── CloudWatch Agent stub config ────────────────────────────────────────────── -# Install the CloudWatch agent so operators can enable it post-launch -dnf install -y amazon-cloudwatch-agent || true +# ── CloudWatch Agent — installed, configured, and enabled at boot ──────────── +# Requires the instance's IAM role to include CloudWatchAgentServerPolicy +# (see docs/s3-backup-setup.md Step 5). Without it the agent starts but +# fails to publish metrics/logs — check /var/log/amazon-cloudwatch-agent.log. +dnf install -y amazon-cloudwatch-agent mkdir -p /opt/aws/amazon-cloudwatch-agent/etc/ cp /tmp/config/cloudwatch/mediawiki-cwa.json /opt/aws/amazon-cloudwatch-agent/etc/mediawiki-cwa.json +/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \ + -a fetch-config -m ec2 -s \ + -c file:/opt/aws/amazon-cloudwatch-agent/etc/mediawiki-cwa.json + +systemctl enable amazon-cloudwatch-agent + # ── Install scripts on the AMI ──────────────────────────────────────────────── mkdir -p /opt/mediawiki-ami # (files already copied by Packer's file provisioner) diff --git a/packer/scripts/07-backup-setup.sh b/packer/scripts/07-backup-setup.sh index 0ae77de..d330b32 100755 --- a/packer/scripts/07-backup-setup.sh +++ b/packer/scripts/07-backup-setup.sh @@ -3,7 +3,7 @@ # Phase 7: Install automated backup cron with GFS (Grandfather-Father-Son) retention. # # This script installs: -# - The backup-with-retention.sh script at /opt/mediawiki-ami/backup/ +# - The backup-with-retention.sh script at /opt/mediawiki-ami/ # - A cron job that runs nightly at 02:00 UTC # - Log rotation for backup logs # diff --git a/scripts/backup/backup-with-retention.sh b/scripts/backup/backup-with-retention.sh index a52a48c..e038df1 100755 --- a/scripts/backup/backup-with-retention.sh +++ b/scripts/backup/backup-with-retention.sh @@ -15,10 +15,10 @@ # Or set environment variables directly. # # Cron usage (already installed by 07-backup-setup.sh): -# 0 2 * * * root /opt/mediawiki-ami/backup/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1 +# 0 2 * * * root /opt/mediawiki-ami/backup-with-retention.sh >> /var/log/mediawiki-backup.log 2>&1 # # Manual usage: -# sudo bash /opt/mediawiki-ami/backup/backup-with-retention.sh +# sudo bash /opt/mediawiki-ami/backup-with-retention.sh set -euo pipefail @@ -194,4 +194,4 @@ log " Daily: s3://${BACKUP_BUCKET}/backups/daily/" [ "${DOW}" = "7" ] && log " Weekly: s3://${BACKUP_BUCKET}/backups/weekly/${WEEK_NUM}/" [ "${DOM}" = "01" ] && log " Monthly: s3://${BACKUP_BUCKET}/backups/monthly/${MONTH_TAG}/" log " Retention is managed by S3 Lifecycle Rules — see docs/s3-backup-setup.md" -log " To restore: sudo BACKUP_BUCKET=${BACKUP_BUCKET} bash /opt/mediawiki-ami/restore/restore.sh" +log " To restore: sudo BACKUP_BUCKET=${BACKUP_BUCKET} bash /opt/mediawiki-ami/restore.sh" diff --git a/scripts/backup/db-backup.sh b/scripts/backup/db-backup.sh index 0fda6d6..8b2fa0f 100644 --- a/scripts/backup/db-backup.sh +++ b/scripts/backup/db-backup.sh @@ -10,7 +10,7 @@ # MW_ROOT, MW_DB_NAME, MW_DB_USER, MW_DB_PASS, MW_DB_HOST, AWS_REGION # # Cron example (nightly at 02:00 UTC): -# 0 2 * * * root BACKUP_BUCKET=my-bucket bash /opt/mediawiki-ami/scripts/backup/db-backup.sh >> /var/log/mw-db-backup.log 2>&1 +# 0 2 * * * root BACKUP_BUCKET=my-bucket bash /opt/mediawiki-ami/db-backup.sh >> /var/log/mw-db-backup.log 2>&1 set -euo pipefail diff --git a/scripts/restore/restore.sh b/scripts/restore/restore.sh index 5c9d8bc..2875846 100755 --- a/scripts/restore/restore.sh +++ b/scripts/restore/restore.sh @@ -190,15 +190,15 @@ fi # ── Step 5: Fix permissions ─────────────────────────────────────────────────── log "--- Step 5: Fixing permissions ---" -chown -R apache:apache "${MW_ROOT}/images" 2>/dev/null || \ - chown -R www-data:www-data "${MW_ROOT}/images" 2>/dev/null || true -chmod -R 755 "${MW_ROOT}/images" 2>/dev/null || true +chown -R apache:apache "${MW_ROOT}/images" 2>/dev/null +find "${MW_ROOT}/images" -type d -exec chmod 755 {} \; +find "${MW_ROOT}/images" -type f -exec chmod 644 {} \; chmod 640 "${MW_ROOT}/LocalSettings.php" -chown root:apache "${MW_ROOT}/LocalSettings.php" 2>/dev/null || true +chown root:apache "${MW_ROOT}/LocalSettings.php" 2>/dev/null log "=== Restore complete ===" log " DB: ${MW_DB_NAME} on ${MW_DB_HOST}" log " Images: ${MW_ROOT}/images/" log "" log "If this was a restore of 1.35 data onto a 1.43 AMI, run next:" -log " sudo bash /opt/mediawiki-ami/restore/upgrade-1.35-to-1.43.sh" +log " sudo bash /opt/mediawiki-ami/upgrade-1.35-to-1.43.sh" diff --git a/scripts/setup/setup-ssl.sh b/scripts/setup/setup-ssl.sh index 486d8a3..be20805 100644 --- a/scripts/setup/setup-ssl.sh +++ b/scripts/setup/setup-ssl.sh @@ -5,7 +5,7 @@ # Run once after the instance has its final IP/DNS and port 80/443 are # reachable from the internet: # -# sudo bash /opt/mediawiki-ami/setup/setup-ssl.sh +# sudo bash /opt/mediawiki-ami/setup-ssl.sh # # After this script completes: # - Apache serves the wiki over HTTPS with a valid cert @@ -23,6 +23,12 @@ EMAIL="${EMAIL:-admin@asmbly.org}" log() { echo "[$(date -u +"%Y-%m-%d %H:%M:%S")] $*"; } +log "Setting system hostname to ${DOMAIN}..." +hostnamectl set-hostname "${DOMAIN}" +if ! grep -q "127.0.0.1 ${DOMAIN}" /etc/hosts; then + echo "127.0.0.1 ${DOMAIN}" >> /etc/hosts +fi + log "Obtaining Let's Encrypt certificate for ${DOMAIN}..." certbot --apache \ --non-interactive \ @@ -50,4 +56,3 @@ systemctl enable --now certbot-renew.timer log "Auto-renewal enabled (renews 30 days before expiry — at the 60-day mark)" log "Timer status: $(systemctl is-enabled certbot-renew.timer)" log "Next run: $(systemctl list-timers certbot-renew.timer --no-pager 2>/dev/null | awk 'NR==2{print $1,$2}')" -