diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0ee6d261e..f4d85660d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,6 +19,18 @@ on: schedule: - cron: '19 20 * * 5' +env: + CGO_CFLAGS: -I/home/runner/go/deps/dqlite/include/ + CGO_LDFLAGS: -L/home/runner/go/deps/dqlite/.libs/ + LD_LIBRARY_PATH: /home/runner/go/deps/dqlite/.libs/ + CGO_LDFLAGS_ALLOW: (-Wl,-wrap,pthread_create)|(-Wl,-z,now) + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: analyze: if: ${{ github.repository_owner == 'canonical' }} @@ -41,7 +53,7 @@ jobs: strategy: fail-fast: false matrix: - language: [ 'go' ] + language: ['actions', 'go'] # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both @@ -53,6 +65,12 @@ jobs: with: persist-credentials: false + - name: Install Go + if: matrix.language == 'go' + uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 + with: + go-version-file: 'go.mod' + # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 @@ -65,21 +83,20 @@ jobs: # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality - # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). - # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild + if: matrix.language != 'go' uses: github/codeql-action/autobuild@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 - # ℹ️ Command-line programs to run using the OS shell. - # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - - # If the Autobuild fails above, remove it and uncomment the following three lines. - # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. + - name: Install MicroCloud build dependencies + if: matrix.language == 'go' + uses: ./.github/actions/install-builddeps - # - run: | - # echo "Run, Build Application using script" - # ./location_of_script_within_repo/buildscript.sh + - name: Build + if: matrix.language == 'go' + run: | + make deps + make build - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 9987595a6..ee3fefe09 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -77,15 +77,10 @@ jobs: strategy: matrix: include: - - channel: "3/edge" - branch: "main" - version: "3" - - channel: "2/stable" - branch: "v2-edge" - version: "2" - - channel: "1/stable" - branch: "v1-edge" - version: "1" + - track: 3 + branch: main + - track: 2 + branch: v2-edge steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -101,7 +96,7 @@ jobs: env: SNAP_NAME: ${{ github.event.repository.name }} run: | - snap download "${SNAP_NAME}" --channel=${{ matrix.version }}/stable --cohort="+" + snap download "${SNAP_NAME}" --channel=${{ matrix.track }}/stable --cohort="+" unsquashfs ./${SNAP_NAME}*.snap - name: Run Trivy vulnerability scanner @@ -112,7 +107,7 @@ jobs: scanners: vuln,secret,misconfig format: sarif severity: LOW,MEDIUM,HIGH,CRITICAL - output: ${{ matrix.version }}-stable.sarif + output: ${{ matrix.branch }}.sarif - name: Flag snap scanning alerts and tag KEV alerts run: | @@ -132,12 +127,12 @@ jobs: else . end - )' ${{ matrix.version }}-stable.sarif > ${{ matrix.version }}-modified.sarif - mv ${{ matrix.version }}-modified.sarif ${{ matrix.version }}-stable.sarif + )' ${{ matrix.branch }}.sarif > ${{ matrix.branch }}-modified.sarif + mv ${{ matrix.branch }}-modified.sarif ${{ matrix.branch }}.sarif - name: Upload Trivy scan results to GitHub Security tab uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4 with: - sarif_file: ${{ matrix.version }}-stable.sarif + sarif_file: ${{ matrix.branch }}.sarif sha: ${{ steps.branch-sha.outputs.sha }} ref: refs/heads/${{ matrix.branch }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 00aadb5d3..1016a4191 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -100,11 +100,6 @@ jobs: make deps make build-test - # Include dqlite libs in dependencies for system tests. - mkdir /home/runner/go/bin/dqlite - cp -r ~/go/deps/dqlite/include /home/runner/go/bin/dqlite/include - cp -r ~/go/deps/dqlite/.libs /home/runner/go/bin/dqlite/libs - - name: Run static analysis run: make check-static @@ -124,6 +119,13 @@ jobs: path: ${{env.GOCOVERDIR}} if: env.GOCOVERDIR != '' + - name: Prepare dqlite dependencies for system tests + run: | + # Include dqlite libs in dependencies for system tests. + mkdir /home/runner/go/bin/dqlite + mv ~/go/deps/dqlite/include /home/runner/go/bin/dqlite/include + mv ~/go/deps/dqlite/.libs /home/runner/go/bin/dqlite/libs + - name: Upload system test dependencies uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: @@ -141,7 +143,7 @@ jobs: SNAPSHOT_RESTORE: "1" if: ${{ github.repository_owner == 'canonical' }} name: System (core) - runs-on: self-hosted-linux-amd64-noble-large + runs-on: ubuntu-24.04 needs: code-tests strategy: fail-fast: false @@ -196,7 +198,7 @@ jobs: SNAPSHOT_RESTORE: "1" if: ${{ github.repository_owner == 'canonical' }} name: System (upgrade) - runs-on: self-hosted-linux-amd64-noble-large + runs-on: ubuntu-24.04 needs: code-tests strategy: fail-fast: false diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..9c008f2a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,115 @@ +# AGENTS.md — MicroCloud Agent Instructions + +MicroCloud is an open source cloud platform written in Go. It orchestrates a cluster of machines by auto-configuring LXD, MicroCeph, and MicroOVN. Module: `github.com/canonical/microcloud/microcloud`. + +## Prerequisites + +MicroCloud requires Go 1.26.2 or higher. + +- CGO native dependency: `dqlite`. Fetch and build it once with: + + ``` + make deps + ``` + + This clones and builds the `dqlite` C library under `$GOPATH/deps/dqlite`. Set `CGO_CFLAGS`, `CGO_LDFLAGS`, and `LD_LIBRARY_PATH` accordingly if building outside of `make`. + +## Repository layout + +``` +api/ HTTP API handlers and request/response types + types/ Shared API type definitions +client/ Go client library for the MicroCloud API +cmd/ + microcloud/ CLI binary + microcloudd/ Daemon binary + tui/ Terminal UI library (tables, prompts, autocomplete) +database/ dqlite schema and CRUD helpers +multicast/ UDP multicast peer discovery +service/ Service interface and wrappers for LXD, MicroCeph, MicroOVN +test/ + suites/ System test suites (bash) + includes/ Shared shell helper functions + lint/ Shell lint scripts + e2e/ Post-deployment end-to-end tests (Terraform) +version/ Single source of truth for the version string +doc/ Sphinx documentation +``` + +### Auto-generated files — do not edit manually + +Update these via the listed `make` target instead of editing by hand: + +| File | Command | +| --- | --- | +| `go.mod`, `go.sum` | `make update-gomod` | + +## Build + +```sh +# Production build +make build + +# Test build (scripted TUI input, simplified wordlist) +make build-test +``` + +## Validate before committing + +Run these in order. Each must pass before moving to the next. + +```sh +# 1. Static analysis (golangci-lint, revive, shell lint scripts) +make check-static + +# 2. Unit tests +make check-unit + +# 3. Full build +make build +``` + +`make check-static` runs `golangci-lint`, `revive`, and the shell scripts under `test/lint/`. Review any reformatted files and stage only changes relevant to your work. + +## Key conventions + +### Commit format + +``` +: +``` + +Examples: +- `api/services: Use the authHandlerMTLS func` +- `cmd/microcloud: Remove token add command` +- `service/lxd: Fix storage pool bootstrap error handling` + +Use separate commits for each logical change and for changes to different components. See `CONTRIBUTING.md` for DCO sign-off (`git commit -s`) and GPG signature requirements. + +### Error messages + +- Use `"Cannot"` not `"Unable to"`. +- Capitalize the first letter of error strings: `fmt.Errorf("Cannot connect to ...")`. +- No contractions: `"does not"` not `"doesn't"`. +- US English spelling throughout (`behavior`, `color`, `initialize`, `organization`). + +### Go code style + +- No inline variable declarations inside `if` conditions — assign on a separate line first. +- Prefer early returns to reduce nesting. +- Import grouping (enforced by `gci`): stdlib → external → `github.com/canonical/microcloud/microcloud`. +- Check `service/` for existing helpers before implementing utilities from scratch. +- Both the `microcloud` CLI and `microcloudd` daemon enforce `os.Geteuid() == 0`; keep this behavior. + +### Shell test style + +- Use `jq --exit-status` (`jq -e`) when asserting field presence or values. +- For expected command failure: `if cmd_should_fail; then echo "ERROR: ..."; exit 1; fi` +- Use helper functions from `test/includes/microcloud.sh` (`validate_system_*`, `reset_systems`, etc.) rather than reimplementing validation logic. + +### Build tags + +| Tag | Purpose | +| --- | --- | +| `agent` | Production build | +| `test` | Enables `TEST_CONSOLE=1` scripted input and replaces the EFF wordlist with a small test wordlist | diff --git a/cmd/microcloud/ask.go b/cmd/microcloud/ask.go index f2aba5766..a97ea1c8a 100644 --- a/cmd/microcloud/ask.go +++ b/cmd/microcloud/ask.go @@ -1106,8 +1106,12 @@ func (c *initConfig) askOVNNetwork(sh *service.Handler) error { } if warningMessage != "" { + // Be concrete and notify the user that we cannot configure MicroOVN at this stage. + tui.PrintWarning("Cannot configure MicroOVN") + + // Ask to continue and print a second warning here (the actual warning message) which contains some more reasoning. question := "Continue anyway?" - wantsContinue, err := c.asker.AskBoolWarn(warningMessage, question, true) + wantsContinue, err := c.asker.AskBoolWarn(warningMessage, question, false) if err != nil { return err } diff --git a/demos/terraform/main.tf b/demos/terraform/main.tf index 503169cee..25dc7a43a 100644 --- a/demos/terraform/main.tf +++ b/demos/terraform/main.tf @@ -32,20 +32,17 @@ locals { systems = [ for i, name in var.vm_names : { - name = name - ip = cidrhost(local.lookup_subnet, var.ip_base_offset + (i * var.ip_increment)) - has_ceph = contains(var.ceph_nodes, name) + name = name + ip = cidrhost(local.lookup_subnet, var.ip_base_offset + (i * var.ip_increment)) + has_ceph = contains(var.ceph_nodes, name) + disk_number = var.disk_number_start + i } ] - ceph_disk_mapping = { - for i, system in local.systems : i => length([ - for j, s in local.systems : j if j < i && s.has_ceph - ]) if system.has_ceph - } + ceph_systems = [for s in local.systems : s if s.has_ceph] } -resource "lxd_network" "microbr0" { +resource "lxd_network" "microbr" { name = var.network_name config = { @@ -57,8 +54,8 @@ resource "lxd_network" "microbr0" { } resource "lxd_volume" "local_disk" { - count = var.vm_count - name = "${var.local_disk_name_prefix}${count.index + 1}" + count = length(local.systems) + name = "${var.local_disk_name_prefix}${local.systems[count.index].disk_number}" pool = var.storage_pool type = "custom" content_type = "block" @@ -68,8 +65,8 @@ resource "lxd_volume" "local_disk" { } resource "lxd_volume" "ceph_disk" { - count = length([for system in local.systems : system if system.has_ceph]) - name = "${var.ceph_disk_name_prefix}${count.index + 1}" + count = length(local.ceph_systems) + name = "${var.ceph_disk_name_prefix}${local.ceph_systems[count.index].disk_number}" pool = var.storage_pool type = "custom" content_type = "block" @@ -79,7 +76,7 @@ resource "lxd_volume" "ceph_disk" { } resource "lxd_instance" "microcloud" { - count = var.vm_count + count = length(local.systems) name = var.vm_names[count.index] image = var.ubuntu_image type = "virtual-machine" @@ -90,7 +87,6 @@ resource "lxd_instance" "microcloud" { } config = { - "security.secureboot" = "false" "cloud-init.user-data" = templatefile("${path.module}/cloud-init.yaml.tpl", { hostname = var.vm_names[count.index] lookup_interface = var.lookup_interface @@ -123,7 +119,7 @@ resource "lxd_instance" "microcloud" { type = "nic" properties = { nictype = "bridged" - parent = lxd_network.microbr0.name + parent = lxd_network.microbr.name } } @@ -143,7 +139,7 @@ resource "lxd_instance" "microcloud" { type = "disk" properties = { pool = var.storage_pool - source = lxd_volume.ceph_disk[local.ceph_disk_mapping[count.index]].name + source = lxd_volume.ceph_disk[index(local.ceph_systems[*].name, local.systems[count.index].name)].name } } } @@ -204,7 +200,7 @@ resource "lxd_instance" "microcloud" { depends_on = [ data.lxd_network.lookup_bridge, - lxd_network.microbr0, + lxd_network.microbr, lxd_volume.local_disk, lxd_volume.ceph_disk ] diff --git a/demos/terraform/terraform.tfvars.example b/demos/terraform/terraform.tfvars.example index a82a4bd6a..f8f58427d 100644 --- a/demos/terraform/terraform.tfvars.example +++ b/demos/terraform/terraform.tfvars.example @@ -22,7 +22,6 @@ lookup_timeout = 300 instance_create_timeout = "25m" # VM Configuration -vm_count = 4 vm_names = ["micro1", "micro2", "micro3", "micro4"] ceph_nodes = ["micro1", "micro2", "micro3"] @@ -46,6 +45,7 @@ ip_increment = 10 # Disk Name Configuration local_disk_name_prefix = "local" ceph_disk_name_prefix = "remote" +disk_number_start = 1 # MicroCloud Configuration initiator = "micro1" diff --git a/demos/terraform/variables.tf b/demos/terraform/variables.tf index 9525e96c0..662ae46ec 100644 --- a/demos/terraform/variables.tf +++ b/demos/terraform/variables.tf @@ -148,13 +148,6 @@ variable "lookup_bridge" { default = "lxdbr0" } - -variable "vm_count" { - description = "Number of VMs to create" - type = number - default = 4 -} - variable "ip_base_offset" { description = "Base IP address offset for the first VM" type = number @@ -179,6 +172,12 @@ variable "ceph_disk_name_prefix" { default = "remote" } +variable "disk_number_start" { + description = "Starting number for disk names (e.g., 1 for local1/remote1, 5 for local5/remote5)" + type = number + default = 1 +} + variable "initiator" { description = "Name of the initiator node for MicroCloud cluster (matches preseed initiator)" type = string diff --git a/doc/conf.py b/doc/conf.py index d43895e78..41d02719c 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -132,10 +132,14 @@ 'http://localhost:8000', # These links may fail from time to time 'https://ceph.io', + # Rejects us always with a "Connection aborted" + r'https://charmhub\.io/.*', # Cloudflare protection on SourceForge domains often block linkcheck r'https://.*\.sourceforge\.(net|io)/.*', # These links often fail due to infra issues or protection measures r'https://ubuntu\.com.*', + r'https://canonical\.com.*', + r'https://snapcraft\.io.*', # Ignore so that we can link change log in release notes before a release is ready r'https://github\.com/canonical/microcloud/compare.*', ] diff --git a/doc/explanation/microcloud.md b/doc/explanation/microcloud.md index b9c3371bf..3f370c0a0 100644 --- a/doc/explanation/microcloud.md +++ b/doc/explanation/microcloud.md @@ -10,9 +10,7 @@ myst: The MicroCloud snap drives three other snaps ({doc}`lxd:index`, {doc}`microceph:index`, and {doc}`microovn:index`), enabling automated deployment of a highly available LXD cluster for compute, with Ceph as the storage driver and OVN as the managed network. -During initialization, MicroCloud scrapes the other servers for details and then prompts you to add disks to Ceph and configure the networking setup. - -At the end of this, you’ll have an OVN cluster, a Ceph cluster, and a LXD cluster. LXD itself will have been configured with both networking and storage suitable for use in a cluster. +MicroCloud starts with a single cluster member, requires 3 members for {ref}`high availability `, and can scale to 50-member clusters. During initialization, MicroCloud scrapes the other servers for details and then prompts you to add disks to Ceph and configure the networking setup. At the end of this, you’ll have an OVN cluster, a Ceph cluster, and a LXD cluster. LXD itself will have been configured with both networking and storage suitable for use in a cluster. (exp-microcloud-lxd)= ## LXD cluster diff --git a/doc/how-to/initialize.md b/doc/how-to/initialize.md index 8d082a880..9c9076ef7 100644 --- a/doc/how-to/initialize.md +++ b/doc/how-to/initialize.md @@ -276,18 +276,18 @@ lxc storage volume create remote-fs backups # for instances operations lxc storage volume create remote-fs images # for images operations ``` -Once the storage volumes are created, set them as the values for the {ref}`configuration options ` of `storage.images_volume` and `storage.backups_volume`. This example syntax uses `remote-fs` as the storage pool, along with volumes named `images` and `backup`: +Once the storage volumes are created, set them as the values for the `storage.images_volume` and `storage.backups_volume` {ref}`configuration options `. +This example syntax uses `remote-fs` as the storage pool, along with volumes named `images` and `backups`. +Repeat these steps for each cluster member: ```bash -lxc config set storage.images_volume=remote-fs/images -lxc config set storage.backups_volume=remote-fs/backups +lxc config set storage.images_volume=remote-fs/images --target +lxc config set storage.backups_volume=remote-fs/backups --target ``` To view the set values, run: ```bash -lxc config get storage.backups_volume -lxc config get storage.images_volume +lxc config get storage.backups_volume --target +lxc config get storage.images_volume --target ``` - -Repeat these steps on each cluster member. diff --git a/doc/reference/releases-snaps.md b/doc/reference/releases-snaps.md index 72406b500..1610c8f9c 100644 --- a/doc/reference/releases-snaps.md +++ b/doc/reference/releases-snaps.md @@ -94,7 +94,7 @@ MicroCloud LTS tracks use the format _x.y_, corresponding to the major and minor The MicroCloud feature track uses the major number of the current {ref}`feature release ` series. Feature releases within the same major version are published to the same track, replacing the previous release. This simplifies updates, as you don't need to switch channels to access new feature releases within the same major version. -The current feature track is {{current_feature_track}}. No feature release has yet been published to this track. The most recent development updates can be found in the {{current_feature_track}}/`edge` channel, for testing purposes only. +The current feature track is {{current_feature_track}}. The most recent development updates can be found in the {{current_feature_track}}/edge channel, for testing purposes only. (ref-snaps-microcloud-track-default)= #### The default track diff --git a/service/microceph.go b/service/microceph.go index 9b827c3ad..ed1c4e143 100644 --- a/service/microceph.go +++ b/service/microceph.go @@ -22,6 +22,9 @@ import ( cloudClient "github.com/canonical/microcloud/microcloud/client" ) +// cephJobTimeout is the maximum time we allow for a Ceph job to complete before the context gets canceled. +const cephJobTimeout = 5 * time.Minute + // CephService is a MicroCeph service. type CephService struct { m *microcluster.MicroCluster @@ -148,7 +151,7 @@ func (s CephService) AddDisk(ctx context.Context, data cephTypes.DisksPost, targ // In the pipeline runners we often see runtimes > 1 minute. // To prevent any issues with slow environments set a more forgiving upper limit. // As long as the MicroCeph API doesn't return an error, the process is still running and we have to wait for it. - ctx, cancel = context.WithTimeout(ctx, 5*time.Minute) + ctx, cancel = context.WithTimeout(ctx, cephJobTimeout) defer cancel() err = c.Query(ctx, "POST", types.APIVersion, api.NewURL().Path("disks"), data, &response) @@ -232,6 +235,11 @@ func (s CephService) PoolSetReplicationFactor(ctx context.Context, data cephType return err } + // On the GH runners it was observed that setting the pool replication factor can take + // longer than the default of 30s if no deadline is set on the context. + ctx, cancel := context.WithTimeout(ctx, cephJobTimeout) + defer cancel() + err = c.Query(ctx, "PUT", types.APIVersion, api.NewURL().Path("pools-op"), data, nil) if err != nil { return fmt.Errorf("Failed setting replication factor: %w", err) diff --git a/test/e2e/reboot.local-lxd-vm b/test/e2e/reboot.local-lxd-vm index addbf9e85..b914ad2ed 100755 --- a/test/e2e/reboot.local-lxd-vm +++ b/test/e2e/reboot.local-lxd-vm @@ -33,14 +33,3 @@ lxc exec "${MEMBER}" -- timeout 90 systemctl is-system-running --wait --quiet || sleep 5 lxc exec "${MEMBER}" --env TEST_CONSOLE=0 -- microcloud waitready --timeout=90 || true echo " DONE" - -# Temporary fix to deal with https://github.com/canonical/microovn/pull/249. -# -# First try to find the cluster member which joined last and restart its ovn-northd service. -# We can identify it based on its appearance in Microcluster's `core_cluster_members` table. -# The CLI's `microovn cluster list` cannot be used as the result is sorted. -# MicroCloud doesn't guarantee any order in which members are joined and doesn't sort them by name (as they are internally stored inside a map). -last_member="$(lxc exec "${MEMBER}" -- microovn cluster sql 'SELECT name FROM core_cluster_members ORDER BY ID DESC LIMIT 1' | awk -F" " '/micro/ { print $2 }')" - -# Second restart ovn-northd on this member. -lxc exec "${last_member}" -- systemctl restart snap.microovn.ovn-northd.service diff --git a/test/e2e/versions.tf b/test/e2e/versions.tf index ddcbeb012..ccec0f917 100644 --- a/test/e2e/versions.tf +++ b/test/e2e/versions.tf @@ -3,7 +3,7 @@ terraform { required_providers { lxd = { source = "terraform-lxd/lxd" - version = ">= 2.5.0" + version = "2.7.1" } } }