From 18fa3ead784855ddf5e713d78b6b451042adbbea Mon Sep 17 00:00:00 2001
From: Utku Ozdemir <utku.ozdemir@siderolabs.com>
Date: Mon, 14 Oct 2024 11:45:28 +0200
Subject: [PATCH] feat: implement Talos agent

Add initial implementation of the Talos agent mode service.

Related to siderolabs/omni#660.

Signed-off-by: Utku Ozdemir <utku.ozdemir@siderolabs.com>
---
 .codecov.yml                          |  18 +
 .conform.yaml                         |  48 ++
 .dockerignore                         |  13 +
 .github/workflows/ci.yaml             | 126 +++++
 .github/workflows/slack-notify.yaml   |  92 ++++
 .gitignore                            |   5 +
 .golangci.yml                         | 150 ++++++
 .kres.yaml                            |  79 +++
 .license-header.go.txt                |   3 +
 .markdownlint.json                    |   9 +
 Dockerfile                            | 194 ++++++++
 LICENSE                               | 373 ++++++++++++++
 Makefile                              | 227 +++++++++
 README.md                             |   5 +
 api/provider/provider.pb.go           | 187 +++++++
 api/provider/provider.proto           |  18 +
 api/provider/provider_grpc.pb.go      | 122 +++++
 api/provider/provider_vtproto.pb.go   | 321 ++++++++++++
 api/specs/specs.pb.go                 | 267 ++++++++++
 api/specs/specs.proto                 |  23 +
 api/specs/specs_vtproto.pb.go         | 680 ++++++++++++++++++++++++++
 cmd/provider/main.go                  | 167 +++++++
 go.mod                                |  85 ++++
 go.sum                                | 341 +++++++++++++
 hack/release.sh                       | 149 ++++++
 hack/release.toml                     |  11 +
 internal/agent/controller.go          |  84 ++++
 internal/config/config.go             |  86 ++++
 internal/constants/constants.go       |  14 +
 internal/debug/debug.go               |   6 +
 internal/debug/disabled.go            |  10 +
 internal/debug/enabled.go             |  10 +
 internal/dhcp/dhcp.go                 |   6 +
 internal/dhcp/proxy.go                | 240 +++++++++
 internal/imagefactory/imagefactory.go | 128 +++++
 internal/ip/ip.go                     |  51 ++
 internal/ipxe/ipxe.go                 | 137 ++++++
 internal/ipxe/patch.go                | 205 ++++++++
 internal/meta/meta.go                 |   9 +
 internal/omni/omni.go                 | 188 +++++++
 internal/power/api/api.go             |  60 +++
 internal/power/ipmi/ipmi.go           |  49 ++
 internal/power/power.go               |  70 +++
 internal/provider/data/icon.svg       |   1 +
 internal/provider/provider.go         | 140 ++++++
 internal/resources/machine.go         |  49 ++
 internal/server/server.go             | 132 +++++
 internal/service/service.go           |  53 ++
 internal/tftp/tftp_server.go          | 124 +++++
 internal/version/data/sha             |   1 +
 internal/version/data/tag             |   1 +
 internal/version/version.go           |  41 ++
 52 files changed, 5608 insertions(+)
 create mode 100644 .codecov.yml
 create mode 100644 .conform.yaml
 create mode 100644 .dockerignore
 create mode 100644 .github/workflows/ci.yaml
 create mode 100644 .github/workflows/slack-notify.yaml
 create mode 100644 .gitignore
 create mode 100644 .golangci.yml
 create mode 100644 .kres.yaml
 create mode 100644 .license-header.go.txt
 create mode 100644 .markdownlint.json
 create mode 100644 Dockerfile
 create mode 100644 LICENSE
 create mode 100644 Makefile
 create mode 100644 api/provider/provider.pb.go
 create mode 100644 api/provider/provider.proto
 create mode 100644 api/provider/provider_grpc.pb.go
 create mode 100644 api/provider/provider_vtproto.pb.go
 create mode 100644 api/specs/specs.pb.go
 create mode 100644 api/specs/specs.proto
 create mode 100644 api/specs/specs_vtproto.pb.go
 create mode 100644 cmd/provider/main.go
 create mode 100644 go.mod
 create mode 100644 go.sum
 create mode 100755 hack/release.sh
 create mode 100644 hack/release.toml
 create mode 100644 internal/agent/controller.go
 create mode 100644 internal/config/config.go
 create mode 100644 internal/constants/constants.go
 create mode 100644 internal/debug/debug.go
 create mode 100644 internal/debug/disabled.go
 create mode 100644 internal/debug/enabled.go
 create mode 100644 internal/dhcp/dhcp.go
 create mode 100644 internal/dhcp/proxy.go
 create mode 100644 internal/imagefactory/imagefactory.go
 create mode 100644 internal/ip/ip.go
 create mode 100644 internal/ipxe/ipxe.go
 create mode 100644 internal/ipxe/patch.go
 create mode 100644 internal/meta/meta.go
 create mode 100644 internal/omni/omni.go
 create mode 100644 internal/power/api/api.go
 create mode 100644 internal/power/ipmi/ipmi.go
 create mode 100644 internal/power/power.go
 create mode 100644 internal/provider/data/icon.svg
 create mode 100644 internal/provider/provider.go
 create mode 100644 internal/resources/machine.go
 create mode 100644 internal/server/server.go
 create mode 100644 internal/service/service.go
 create mode 100644 internal/tftp/tftp_server.go
 create mode 100644 internal/version/data/sha
 create mode 100644 internal/version/data/tag
 create mode 100644 internal/version/version.go

diff --git a/.codecov.yml b/.codecov.yml
new file mode 100644
index 0000000..805d898
--- /dev/null
+++ b/.codecov.yml
@@ -0,0 +1,18 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+codecov:
+  require_ci_to_pass: false
+
+coverage:
+  status:
+    project:
+      default:
+        target: 0%
+        threshold: 0.5%
+        base: auto
+        if_ci_failed: success
+    patch: off
+
+comment: false
diff --git a/.conform.yaml b/.conform.yaml
new file mode 100644
index 0000000..333383f
--- /dev/null
+++ b/.conform.yaml
@@ -0,0 +1,48 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+policies:
+  - type: commit
+    spec:
+      dco: true
+      gpg:
+        required: true
+        identity:
+          gitHubOrganization: siderolabs
+      spellcheck:
+        locale: US
+      maximumOfOneCommit: true
+      header:
+        length: 89
+        imperative: true
+        case: lower
+        invalidLastCharacters: .
+      body:
+        required: true
+      conventional:
+        types:
+          - chore
+          - docs
+          - perf
+          - refactor
+          - style
+          - test
+          - release
+        scopes:
+          - .*
+  - type: license
+    spec:
+      root: .
+      skipPaths:
+        - .git/
+        - testdata/
+      includeSuffixes:
+        - .go
+      excludeSuffixes:
+        - .pb.go
+        - .pb.gw.go
+      header: |
+        // This Source Code Form is subject to the terms of the Mozilla Public
+        // License, v. 2.0. If a copy of the MPL was not distributed with this
+        // file, You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..0523122
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,13 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:34:03Z by kres 34e72ac.
+
+*
+!api
+!cmd
+!internal
+!go.mod
+!go.sum
+!.golangci.yml
+!README.md
+!.markdownlint.json
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
new file mode 100644
index 0000000..8465674
--- /dev/null
+++ b/.github/workflows/ci.yaml
@@ -0,0 +1,126 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-18T21:14:30Z by kres 34e72ac.
+
+name: default
+concurrency:
+  group: ${{ github.head_ref || github.run_id }}
+  cancel-in-progress: true
+"on":
+  push:
+    branches:
+      - main
+      - release-*
+    tags:
+      - v*
+  pull_request:
+    branches:
+      - main
+      - release-*
+jobs:
+  default:
+    permissions:
+      actions: read
+      contents: write
+      issues: read
+      packages: write
+      pull-requests: read
+    runs-on:
+      - self-hosted
+      - generic
+    if: (!startsWith(github.head_ref, 'renovate/') && !startsWith(github.head_ref, 'dependabot/'))
+    steps:
+      - name: gather-system-info
+        id: system-info
+        uses: kenchan0130/actions-system-info@v1.3.0
+        continue-on-error: true
+      - name: print-system-info
+        run: |
+          MEMORY_GB=$((${{ steps.system-info.outputs.totalmem }}/1024/1024/1024))
+
+          OUTPUTS=(
+            "CPU Core: ${{ steps.system-info.outputs.cpu-core }}"
+            "CPU Model: ${{ steps.system-info.outputs.cpu-model }}"
+            "Hostname: ${{ steps.system-info.outputs.hostname }}"
+            "NodeName: ${NODE_NAME}"
+            "Kernel release: ${{ steps.system-info.outputs.kernel-release }}"
+            "Kernel version: ${{ steps.system-info.outputs.kernel-version }}"
+            "Name: ${{ steps.system-info.outputs.name }}"
+            "Platform: ${{ steps.system-info.outputs.platform }}"
+            "Release: ${{ steps.system-info.outputs.release }}"
+            "Total memory: ${MEMORY_GB} GB"
+          )
+
+          for OUTPUT in "${OUTPUTS[@]}";do
+            echo "${OUTPUT}"
+          done
+        continue-on-error: true
+      - name: checkout
+        uses: actions/checkout@v4
+      - name: Unshallow
+        run: |
+          git fetch --prune --unshallow
+      - name: Set up Docker Buildx
+        id: setup-buildx
+        uses: docker/setup-buildx-action@v3
+        with:
+          driver: remote
+          endpoint: tcp://buildkit-amd64.ci.svc.cluster.local:1234
+        timeout-minutes: 10
+      - name: base
+        run: |
+          make base
+      - name: unit-tests
+        run: |
+          make unit-tests
+      - name: unit-tests-race
+        run: |
+          make unit-tests-race
+      - name: coverage
+        uses: codecov/codecov-action@v4
+        with:
+          files: _out/coverage-unit-tests.txt
+          token: ${{ secrets.CODECOV_TOKEN }}
+        timeout-minutes: 3
+      - name: provider
+        run: |
+          make provider
+      - name: lint
+        run: |
+          make lint
+      - name: Login to registry
+        if: github.event_name != 'pull_request'
+        uses: docker/login-action@v3
+        with:
+          password: ${{ secrets.GITHUB_TOKEN }}
+          registry: ghcr.io
+          username: ${{ github.repository_owner }}
+      - name: image-provider
+        run: |
+          make image-provider
+      - name: push-provider
+        if: github.event_name != 'pull_request'
+        env:
+          PLATFORM: linux/amd64,linux/arm64
+          PUSH: "true"
+        run: |
+          make image-provider
+      - name: Generate Checksums
+        if: startsWith(github.ref, 'refs/tags/')
+        run: |
+          cd _out
+          sha256sum provider-* > sha256sum.txt
+          sha512sum provider-* > sha512sum.txt
+      - name: release-notes
+        if: startsWith(github.ref, 'refs/tags/')
+        run: |
+          make release-notes
+      - name: Release
+        if: startsWith(github.ref, 'refs/tags/')
+        uses: crazy-max/ghaction-github-release@v2
+        with:
+          body_path: _out/RELEASE_NOTES.md
+          draft: "true"
+          files: |-
+            _out/provider-*
+            _out/sha*.txt
diff --git a/.github/workflows/slack-notify.yaml b/.github/workflows/slack-notify.yaml
new file mode 100644
index 0000000..22319fe
--- /dev/null
+++ b/.github/workflows/slack-notify.yaml
@@ -0,0 +1,92 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+name: slack-notify
+"on":
+  workflow_run:
+    workflows:
+      - default
+    types:
+      - completed
+jobs:
+  slack-notify:
+    runs-on:
+      - self-hosted
+      - generic
+    if: github.event.workflow_run.conclusion != 'skipped'
+    steps:
+      - name: Get PR number
+        id: get-pr-number
+        if: github.event.workflow_run.event == 'pull_request'
+        env:
+          GH_TOKEN: ${{ github.token }}
+        run: |
+          echo pull_request_number=$(gh pr view -R ${{ github.repository }} ${{ github.event.workflow_run.head_repository.owner.login }}:${{ github.event.workflow_run.head_branch }} --json number --jq .number) >> $GITHUB_OUTPUT
+      - name: Slack Notify
+        uses: slackapi/slack-github-action@v1
+        with:
+          channel-id: proj-talos-maintainers
+          payload: |
+            {
+                "attachments": [
+                    {
+                        "color": "${{ github.event.workflow_run.conclusion == 'success' && '#2EB886' || github.event.workflow_run.conclusion == 'failure' && '#A30002' || '#FFCC00' }}",
+                        "fallback": "test",
+                        "blocks": [
+                            {
+                                "type": "section",
+                                "fields": [
+                                    {
+                                        "type": "mrkdwn",
+                                        "text": "${{ github.event.workflow_run.event == 'pull_request' && format('*Pull Request:* {0} (`{1}`)\n<{2}/pull/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, steps.get-pr-number.outputs.pull_request_number, github.event.workflow_run.display_title) || format('*Build:* {0} (`{1}`)\n<{2}/commit/{3}|{4}>', github.repository, github.ref_name, github.event.repository.html_url, github.sha, github.event.workflow_run.display_title) }}"
+                                    },
+                                    {
+                                        "type": "mrkdwn",
+                                        "text": "*Status:*\n`${{ github.event.workflow_run.conclusion }}`"
+                                    }
+                                ]
+                            },
+                            {
+                                "type": "section",
+                                "fields": [
+                                    {
+                                        "type": "mrkdwn",
+                                        "text": "*Author:*\n`${{ github.actor }}`"
+                                    },
+                                    {
+                                        "type": "mrkdwn",
+                                        "text": "*Event:*\n`${{ github.event.workflow_run.event }}`"
+                                    }
+                                ]
+                            },
+                            {
+                                "type": "divider"
+                            },
+                            {
+                                "type": "actions",
+                                "elements": [
+                                    {
+                                        "type": "button",
+                                        "text": {
+                                            "type": "plain_text",
+                                            "text": "Logs"
+                                        },
+                                        "url": "${{ github.event.workflow_run.html_url }}"
+                                    },
+                                    {
+                                        "type": "button",
+                                        "text": {
+                                            "type": "plain_text",
+                                            "text": "Commit"
+                                        },
+                                        "url": "${{ github.event.repository.html_url }}/commit/${{ github.sha }}"
+                                    }
+                                ]
+                            }
+                        ]
+                    }
+                ]
+            }
+        env:
+          SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7c5e64a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+_out
diff --git a/.golangci.yml b/.golangci.yml
new file mode 100644
index 0000000..200df9e
--- /dev/null
+++ b/.golangci.yml
@@ -0,0 +1,150 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+# options for analysis running
+run:
+  timeout: 10m
+  issues-exit-code: 1
+  tests: true
+  build-tags: [ ]
+  modules-download-mode: readonly
+
+# output configuration options
+output:
+  formats:
+    - format: colored-line-number
+      path: stdout
+  print-issued-lines: true
+  print-linter-name: true
+  uniq-by-line: true
+  path-prefix: ""
+
+# all available settings of specific linters
+linters-settings:
+  dogsled:
+    max-blank-identifiers: 2
+  dupl:
+    threshold: 150
+  errcheck:
+    check-type-assertions: true
+    check-blank: true
+  exhaustive:
+    default-signifies-exhaustive: false
+  gci:
+    sections:
+      - standard # Standard section: captures all standard packages.
+      - default # Default section: contains all imports that could not be matched to another section type.
+      - localmodule # Imports from the same module.
+  gocognit:
+    min-complexity: 30
+  nestif:
+    min-complexity: 5
+  goconst:
+    min-len: 3
+    min-occurrences: 3
+  gocritic:
+    disabled-checks: [ ]
+  gocyclo:
+    min-complexity: 20
+  godot:
+    scope: declarations
+  gofmt:
+    simplify: true
+  gomodguard: { }
+  govet:
+    enable-all: true
+  lll:
+    line-length: 200
+    tab-width: 4
+  misspell:
+    locale: US
+    ignore-words: [ ]
+  nakedret:
+    max-func-lines: 30
+  prealloc:
+    simple: true
+    range-loops: true # Report preallocation suggestions on range loops, true by default
+    for-loops: false # Report preallocation suggestions on for loops, false by default
+  nolintlint:
+    allow-unused: false
+    allow-no-explanation: [ ]
+    require-explanation: false
+    require-specific: true
+  rowserrcheck: { }
+  testpackage: { }
+  unparam:
+    check-exported: false
+  unused:
+    local-variables-are-used: false
+  whitespace:
+    multi-if: false   # Enforces newlines (or comments) after every multi-line if statement
+    multi-func: false # Enforces newlines (or comments) after every multi-line function signature
+  wsl:
+    strict-append: true
+    allow-assign-and-call: true
+    allow-multiline-assign: true
+    allow-cuddle-declarations: false
+    allow-trailing-comment: false
+    force-case-trailing-whitespace: 0
+    force-err-cuddling: false
+    allow-separated-leading-comment: false
+  gofumpt:
+    extra-rules: false
+  cyclop:
+    # the maximal code complexity to report
+    max-complexity: 20
+  depguard:
+    rules:
+      prevent_unmaintained_packages:
+        list-mode: lax # allow unless explicitly denied
+        files:
+          - $all
+        deny:
+          - pkg: io/ioutil
+            desc: "replaced by io and os packages since Go 1.16: https://tip.golang.org/doc/go1.16#ioutil"
+
+linters:
+  enable-all: true
+  disable-all: false
+  fast: false
+  disable:
+    - exhaustruct
+    - err113
+    - forbidigo
+    - funlen
+    - gochecknoglobals
+    - gochecknoinits
+    - godox
+    - gomnd
+    - gomoddirectives
+    - gosec
+    - inamedparam
+    - ireturn
+    - mnd
+    - nestif
+    - nonamedreturns
+    - paralleltest
+    - tagalign
+    - tagliatelle
+    - thelper
+    - varnamelen
+    - wrapcheck
+    - testifylint # complains about our assert recorder and has a number of false positives for assert.Greater(t, thing, 1)
+    - protogetter # complains about us using Value field on typed spec, instead of GetValue which has a different signature
+    - perfsprint # complains about us using fmt.Sprintf in non-performance critical code, updating just kres took too long
+    - goimports # same as gci
+    - musttag # seems to be broken - goes into imported libraries and reports issues there
+
+issues:
+  exclude: [ ]
+  exclude-rules: [ ]
+  exclude-use-default: false
+  exclude-case-sensitive: false
+  max-issues-per-linter: 10
+  max-same-issues: 3
+  new: false
+
+severity:
+  default-severity: error
+  case-sensitive: false
diff --git a/.kres.yaml b/.kres.yaml
new file mode 100644
index 0000000..628d930
--- /dev/null
+++ b/.kres.yaml
@@ -0,0 +1,79 @@
+kind: common.Image
+name: image-metal-agent
+spec:
+  pushLatest: false
+  extraEnvironment:
+    PLATFORM: linux/amd64,linux/arm64
+---
+kind: custom.Step
+name: ipxe
+spec:
+  docker:
+    enabled: true
+    stages:
+      - name: ipxe-linux-amd64
+        from: ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4
+        platform: linux/amd64
+      - name: ipxe-linux-arm64
+        from: ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4
+        platform: linux/arm64
+---
+kind: auto.CustomSteps
+spec:
+  steps:
+    - name: ipxe
+      toplevel: true
+---
+kind: common.Image
+name: image-provider
+spec:
+  pushLatest: false
+  extraEnvironment:
+    PLATFORM: linux/amd64,linux/arm64
+  copyFrom:
+    - stage: ghcr.io/siderolabs/musl:v1.8.0-16-g71d23b4 # required by zbin
+      source: /
+      destination: /
+    - stage: ghcr.io/siderolabs/liblzma:v1.8.0-16-g71d23b4 # required by zbin
+      source: /
+      destination: /
+    - stage: ghcr.io/siderolabs/openssl:v1.8.0-16-g71d23b4 # required by ipmitool
+      source: /
+      destination: /
+    - stage: ghcr.io/siderolabs/ipmitool:v1.8.0-16-g71d23b4
+      source: /
+      destination: /
+    - stage: ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4
+      source: /usr/libexec/zbin
+      destination: /bin/zbin
+    - stage: ipxe-linux-amd64
+      source: /usr/libexec/
+      destination: /var/lib/ipxe/amd64
+    - stage: ipxe-linux-arm64
+      source: /usr/libexec/
+      destination: /var/lib/ipxe/arm64
+---
+kind: golang.Build
+spec:
+  outputs:
+    linux-amd64:
+      GOOS: linux
+      GOARCH: amd64
+    linux-arm64:
+      GOOS: linux
+      GOARCH: arm64
+---
+kind: golang.Generate
+spec:
+  versionPackagePath: internal/version
+  baseSpecPath: /api
+  vtProtobufEnabled: true
+  specs:
+    - source: api/provider/provider.proto
+      subdirectory: provider
+    - source: api/specs/specs.proto
+      subdirectory: specs
+---
+kind: service.CodeCov
+spec:
+  targetThreshold: 0
diff --git a/.license-header.go.txt b/.license-header.go.txt
new file mode 100644
index 0000000..66e0819
--- /dev/null
+++ b/.license-header.go.txt
@@ -0,0 +1,3 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/.markdownlint.json b/.markdownlint.json
new file mode 100644
index 0000000..8631d63
--- /dev/null
+++ b/.markdownlint.json
@@ -0,0 +1,9 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+{
+    "MD013": false,
+    "MD033": false,
+    "default": true
+  }
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..1403318
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,194 @@
+# syntax = docker/dockerfile-upstream:1.10.0-labs
+
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-28T23:32:18Z by kres 6d3cad4.
+
+ARG TOOLCHAIN
+
+FROM ghcr.io/siderolabs/ca-certificates:v1.8.0 AS image-ca-certificates
+
+FROM ghcr.io/siderolabs/fhs:v1.8.0 AS image-fhs
+
+FROM --platform=linux/amd64 ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4 AS ipxe-linux-amd64
+
+FROM --platform=linux/arm64 ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4 AS ipxe-linux-arm64
+
+# runs markdownlint
+FROM docker.io/oven/bun:1.1.32-alpine AS lint-markdown
+WORKDIR /src
+RUN bun i markdownlint-cli@0.42.0 sentences-per-line@0.2.1
+COPY .markdownlint.json .
+COPY ./README.md ./README.md
+RUN bunx markdownlint --ignore "CHANGELOG.md" --ignore "**/node_modules/**" --ignore '**/hack/chglog/**' --rules node_modules/sentences-per-line/index.js .
+
+# collects proto specs
+FROM scratch AS proto-specs
+ADD api/provider/provider.proto /api/provider/
+ADD api/specs/specs.proto /api/specs/
+
+# base toolchain image
+FROM --platform=${BUILDPLATFORM} ${TOOLCHAIN} AS toolchain
+RUN apk --update --no-cache add bash curl build-base protoc protobuf-dev
+
+# build tools
+FROM --platform=${BUILDPLATFORM} toolchain AS tools
+ENV GO111MODULE=on
+ARG CGO_ENABLED
+ENV CGO_ENABLED=${CGO_ENABLED}
+ARG GOTOOLCHAIN
+ENV GOTOOLCHAIN=${GOTOOLCHAIN}
+ARG GOEXPERIMENT
+ENV GOEXPERIMENT=${GOEXPERIMENT}
+ENV GOPATH=/go
+ARG GOIMPORTS_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/tools/cmd/goimports@v${GOIMPORTS_VERSION}
+RUN mv /go/bin/goimports /bin
+ARG PROTOBUF_GO_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install google.golang.org/protobuf/cmd/protoc-gen-go@v${PROTOBUF_GO_VERSION}
+RUN mv /go/bin/protoc-gen-go /bin
+ARG GRPC_GO_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v${GRPC_GO_VERSION}
+RUN mv /go/bin/protoc-gen-go-grpc /bin
+ARG GRPC_GATEWAY_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v${GRPC_GATEWAY_VERSION}
+RUN mv /go/bin/protoc-gen-grpc-gateway /bin
+ARG VTPROTOBUF_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/planetscale/vtprotobuf/cmd/protoc-gen-go-vtproto@v${VTPROTOBUF_VERSION}
+RUN mv /go/bin/protoc-gen-go-vtproto /bin
+ARG DEEPCOPY_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/siderolabs/deep-copy@${DEEPCOPY_VERSION} \
+	&& mv /go/bin/deep-copy /bin/deep-copy
+ARG GOLANGCILINT_VERSION
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCILINT_VERSION} \
+	&& mv /go/bin/golangci-lint /bin/golangci-lint
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg go install golang.org/x/vuln/cmd/govulncheck@latest \
+	&& mv /go/bin/govulncheck /bin/govulncheck
+ARG GOFUMPT_VERSION
+RUN go install mvdan.cc/gofumpt@${GOFUMPT_VERSION} \
+	&& mv /go/bin/gofumpt /bin/gofumpt
+
+# tools and sources
+FROM tools AS base
+WORKDIR /src
+COPY go.mod go.mod
+COPY go.sum go.sum
+RUN cd .
+RUN --mount=type=cache,target=/go/pkg go mod download
+RUN --mount=type=cache,target=/go/pkg go mod verify
+COPY ./api ./api
+COPY ./cmd ./cmd
+COPY ./internal ./internal
+RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null
+
+FROM tools AS embed-generate
+ARG SHA
+ARG TAG
+WORKDIR /src
+RUN mkdir -p internal/version/data && \
+    echo -n ${SHA} > internal/version/data/sha && \
+    echo -n ${TAG} > internal/version/data/tag
+
+# runs protobuf compiler
+FROM tools AS proto-compile
+COPY --from=proto-specs / /
+RUN protoc -I/api --go_out=paths=source_relative:/api --go-grpc_out=paths=source_relative:/api --go-vtproto_out=paths=source_relative:/api --go-vtproto_opt=features=marshal+unmarshal+size+equal+clone /api/provider/provider.proto /api/specs/specs.proto
+RUN rm /api/provider/provider.proto
+RUN rm /api/specs/specs.proto
+RUN goimports -w -local github.com/siderolabs/omni-infra-provider-bare-metal /api
+RUN gofumpt -w /api
+
+# runs gofumpt
+FROM base AS lint-gofumpt
+RUN FILES="$(gofumpt -l .)" && test -z "${FILES}" || (echo -e "Source code is not formatted with 'gofumpt -w .':\n${FILES}"; exit 1)
+
+# runs golangci-lint
+FROM base AS lint-golangci-lint
+WORKDIR /src
+COPY .golangci.yml .
+ENV GOGC=50
+RUN golangci-lint config verify --config .golangci.yml
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/root/.cache/golangci-lint --mount=type=cache,target=/go/pkg golangci-lint run --config .golangci.yml
+
+# runs govulncheck
+FROM base AS lint-govulncheck
+WORKDIR /src
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg govulncheck ./...
+
+# runs unit-tests with race detector
+FROM base AS unit-tests-race
+WORKDIR /src
+ARG TESTPKGS
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp CGO_ENABLED=1 go test -v -race -count 1 ${TESTPKGS}
+
+# runs unit-tests
+FROM base AS unit-tests-run
+WORKDIR /src
+ARG TESTPKGS
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg --mount=type=cache,target=/tmp go test -v -covermode=atomic -coverprofile=coverage.txt -coverpkg=${TESTPKGS} -count 1 ${TESTPKGS}
+
+FROM embed-generate AS embed-abbrev-generate
+WORKDIR /src
+ARG ABBREV_TAG
+RUN echo -n 'undefined' > internal/version/data/sha && \
+    echo -n ${ABBREV_TAG} > internal/version/data/tag
+
+FROM scratch AS unit-tests
+COPY --from=unit-tests-run /src/coverage.txt /coverage-unit-tests.txt
+
+# cleaned up specs and compiled versions
+FROM scratch AS generate
+COPY --from=proto-compile /api/ /api/
+COPY --from=embed-abbrev-generate /src/internal/version internal/version
+
+# builds provider-linux-amd64
+FROM base AS provider-linux-amd64-build
+COPY --from=generate / /
+COPY --from=embed-generate / /
+WORKDIR /src/cmd/provider
+ARG GO_BUILDFLAGS
+ARG GO_LDFLAGS
+ARG VERSION_PKG="internal/version"
+ARG SHA
+ARG TAG
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=amd64 GOOS=linux go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=provider -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /provider-linux-amd64
+
+# builds provider-linux-arm64
+FROM base AS provider-linux-arm64-build
+COPY --from=generate / /
+COPY --from=embed-generate / /
+WORKDIR /src/cmd/provider
+ARG GO_BUILDFLAGS
+ARG GO_LDFLAGS
+ARG VERSION_PKG="internal/version"
+ARG SHA
+ARG TAG
+RUN --mount=type=cache,target=/root/.cache/go-build --mount=type=cache,target=/go/pkg GOARCH=arm64 GOOS=linux go build ${GO_BUILDFLAGS} -ldflags "${GO_LDFLAGS} -X ${VERSION_PKG}.Name=provider -X ${VERSION_PKG}.SHA=${SHA} -X ${VERSION_PKG}.Tag=${TAG}" -o /provider-linux-arm64
+
+FROM scratch AS provider-linux-amd64
+COPY --from=provider-linux-amd64-build /provider-linux-amd64 /provider-linux-amd64
+
+FROM scratch AS provider-linux-arm64
+COPY --from=provider-linux-arm64-build /provider-linux-arm64 /provider-linux-arm64
+
+FROM provider-linux-${TARGETARCH} AS provider
+
+FROM scratch AS provider-all
+COPY --from=provider-linux-amd64 / /
+COPY --from=provider-linux-arm64 / /
+
+FROM scratch AS image-provider
+ARG TARGETARCH
+COPY --from=provider provider-linux-${TARGETARCH} /provider
+COPY --from=image-fhs / /
+COPY --from=image-ca-certificates / /
+COPY --from=ghcr.io/siderolabs/musl:v1.8.0-16-g71d23b4 / /
+COPY --from=ghcr.io/siderolabs/liblzma:v1.8.0-16-g71d23b4 / /
+COPY --from=ghcr.io/siderolabs/openssl:v1.8.0-16-g71d23b4 / /
+COPY --from=ghcr.io/siderolabs/ipmitool:v1.8.0-16-g71d23b4 / /
+COPY --from=ghcr.io/siderolabs/ipxe:v1.8.0-16-g71d23b4 /usr/libexec/zbin /bin/zbin
+COPY --from=ipxe-linux-amd64 /usr/libexec/ /var/lib/ipxe/amd64
+COPY --from=ipxe-linux-arm64 /usr/libexec/ /var/lib/ipxe/arm64
+LABEL org.opencontainers.image.source=https://github.com/siderolabs/omni-infra-provider-bare-metal
+ENTRYPOINT ["/provider"]
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a612ad9
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,373 @@
+Mozilla Public License Version 2.0
+==================================
+
+1. Definitions
+--------------
+
+1.1. "Contributor"
+    means each individual or legal entity that creates, contributes to
+    the creation of, or owns Covered Software.
+
+1.2. "Contributor Version"
+    means the combination of the Contributions of others (if any) used
+    by a Contributor and that particular Contributor's Contribution.
+
+1.3. "Contribution"
+    means Covered Software of a particular Contributor.
+
+1.4. "Covered Software"
+    means Source Code Form to which the initial Contributor has attached
+    the notice in Exhibit A, the Executable Form of such Source Code
+    Form, and Modifications of such Source Code Form, in each case
+    including portions thereof.
+
+1.5. "Incompatible With Secondary Licenses"
+    means
+
+    (a) that the initial Contributor has attached the notice described
+        in Exhibit B to the Covered Software; or
+
+    (b) that the Covered Software was made available under the terms of
+        version 1.1 or earlier of the License, but not also under the
+        terms of a Secondary License.
+
+1.6. "Executable Form"
+    means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+    means a work that combines Covered Software with other material, in
+    a separate file or files, that is not Covered Software.
+
+1.8. "License"
+    means this document.
+
+1.9. "Licensable"
+    means having the right to grant, to the maximum extent possible,
+    whether at the time of the initial grant or subsequently, any and
+    all of the rights conveyed by this License.
+
+1.10. "Modifications"
+    means any of the following:
+
+    (a) any file in Source Code Form that results from an addition to,
+        deletion from, or modification of the contents of Covered
+        Software; or
+
+    (b) any new file in Source Code Form that contains any Covered
+        Software.
+
+1.11. "Patent Claims" of a Contributor
+    means any patent claim(s), including without limitation, method,
+    process, and apparatus claims, in any patent Licensable by such
+    Contributor that would be infringed, but for the grant of the
+    License, by the making, using, selling, offering for sale, having
+    made, import, or transfer of either its Contributions or its
+    Contributor Version.
+
+1.12. "Secondary License"
+    means either the GNU General Public License, Version 2.0, the GNU
+    Lesser General Public License, Version 2.1, the GNU Affero General
+    Public License, Version 3.0, or any later versions of those
+    licenses.
+
+1.13. "Source Code Form"
+    means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+    means an individual or a legal entity exercising rights under this
+    License. For legal entities, "You" includes any entity that
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants and Conditions
+--------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+    Licensable by such Contributor to use, reproduce, make available,
+    modify, display, perform, distribute, and otherwise exploit its
+    Contributions, either on an unmodified basis, with Modifications, or
+    as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+    for sale, have made, import, and otherwise transfer either its
+    Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+    or
+
+(b) for infringements caused by: (i) Your and any other third party's
+    modifications of Covered Software, or (ii) the combination of its
+    Contributions with other software (except as part of its Contributor
+    Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+    its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+    Form, as described in Section 3.1, and You must inform recipients of
+    the Executable Form how they can obtain a copy of such Source Code
+    Form by reasonable means in a timely manner, at a charge no more
+    than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+    License, or sublicense it under different terms, provided that the
+    license for the Executable Form does not attempt to limit or alter
+    the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+*                                                                      *
+*  6. Disclaimer of Warranty                                           *
+*  -------------------------                                           *
+*                                                                      *
+*  Covered Software is provided under this License on an "as is"       *
+*  basis, without warranty of any kind, either expressed, implied, or  *
+*  statutory, including, without limitation, warranties that the       *
+*  Covered Software is free of defects, merchantable, fit for a        *
+*  particular purpose or non-infringing. The entire risk as to the     *
+*  quality and performance of the Covered Software is with You.        *
+*  Should any Covered Software prove defective in any respect, You     *
+*  (not any Contributor) assume the cost of any necessary servicing,   *
+*  repair, or correction. This disclaimer of warranty constitutes an   *
+*  essential part of this License. No use of any Covered Software is   *
+*  authorized under this License except under this disclaimer.         *
+*                                                                      *
+************************************************************************
+
+************************************************************************
+*                                                                      *
+*  7. Limitation of Liability                                          *
+*  --------------------------                                          *
+*                                                                      *
+*  Under no circumstances and under no legal theory, whether tort      *
+*  (including negligence), contract, or otherwise, shall any           *
+*  Contributor, or anyone who distributes Covered Software as          *
+*  permitted above, be liable to You for any direct, indirect,         *
+*  special, incidental, or consequential damages of any character      *
+*  including, without limitation, damages for lost profits, loss of    *
+*  goodwill, work stoppage, computer failure or malfunction, or any    *
+*  and all other commercial damages or losses, even if such party      *
+*  shall have been informed of the possibility of such damages. This   *
+*  limitation of liability shall not apply to liability for death or   *
+*  personal injury resulting from such party's negligence to the       *
+*  extent applicable law prohibits such limitation. Some               *
+*  jurisdictions do not allow the exclusion or limitation of           *
+*  incidental or consequential damages, so this exclusion and          *
+*  limitation may not apply to You.                                    *
+*                                                                      *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+  This Source Code Form is subject to the terms of the Mozilla Public
+  License, v. 2.0. If a copy of the MPL was not distributed with this
+  file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+  This Source Code Form is "Incompatible With Secondary Licenses", as
+  defined by the Mozilla Public License, v. 2.0.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..22f6251
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,227 @@
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-28T23:31:30Z by kres 6d3cad4.
+
+# common variables
+
+SHA := $(shell git describe --match=none --always --abbrev=8 --dirty)
+TAG := $(shell git describe --tag --always --dirty --match v[0-9]\*)
+ABBREV_TAG := $(shell git describe --tags >/dev/null 2>/dev/null && git describe --tag --always --match v[0-9]\* --abbrev=0 || echo 'undefined')
+BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
+ARTIFACTS := _out
+IMAGE_TAG ?= $(TAG)
+OPERATING_SYSTEM := $(shell uname -s | tr '[:upper:]' '[:lower:]')
+GOARCH := $(shell uname -m | sed 's/x86_64/amd64/' | sed 's/aarch64/arm64/')
+WITH_DEBUG ?= false
+WITH_RACE ?= false
+REGISTRY ?= ghcr.io
+USERNAME ?= siderolabs
+REGISTRY_AND_USERNAME ?= $(REGISTRY)/$(USERNAME)
+PROTOBUF_GO_VERSION ?= 1.35.1
+GRPC_GO_VERSION ?= 1.5.1
+GRPC_GATEWAY_VERSION ?= 2.22.0
+VTPROTOBUF_VERSION ?= 0.6.0
+GOIMPORTS_VERSION ?= 0.26.0
+DEEPCOPY_VERSION ?= v0.5.6
+GOLANGCILINT_VERSION ?= v1.61.0
+GOFUMPT_VERSION ?= v0.7.0
+GO_VERSION ?= 1.23.2
+GO_BUILDFLAGS ?=
+GO_LDFLAGS ?=
+CGO_ENABLED ?= 0
+GOTOOLCHAIN ?= local
+TESTPKGS ?= ./...
+KRES_IMAGE ?= ghcr.io/siderolabs/kres:latest
+CONFORMANCE_IMAGE ?= ghcr.io/siderolabs/conform:latest
+
+# docker build settings
+
+BUILD := docker buildx build
+PLATFORM ?= linux/amd64
+PROGRESS ?= auto
+PUSH ?= false
+CI_ARGS ?=
+COMMON_ARGS = --file=Dockerfile
+COMMON_ARGS += --provenance=false
+COMMON_ARGS += --progress=$(PROGRESS)
+COMMON_ARGS += --platform=$(PLATFORM)
+COMMON_ARGS += --push=$(PUSH)
+COMMON_ARGS += --build-arg=ARTIFACTS="$(ARTIFACTS)"
+COMMON_ARGS += --build-arg=SHA="$(SHA)"
+COMMON_ARGS += --build-arg=TAG="$(TAG)"
+COMMON_ARGS += --build-arg=ABBREV_TAG="$(ABBREV_TAG)"
+COMMON_ARGS += --build-arg=USERNAME="$(USERNAME)"
+COMMON_ARGS += --build-arg=REGISTRY="$(REGISTRY)"
+COMMON_ARGS += --build-arg=TOOLCHAIN="$(TOOLCHAIN)"
+COMMON_ARGS += --build-arg=CGO_ENABLED="$(CGO_ENABLED)"
+COMMON_ARGS += --build-arg=GO_BUILDFLAGS="$(GO_BUILDFLAGS)"
+COMMON_ARGS += --build-arg=GO_LDFLAGS="$(GO_LDFLAGS)"
+COMMON_ARGS += --build-arg=GOTOOLCHAIN="$(GOTOOLCHAIN)"
+COMMON_ARGS += --build-arg=GOEXPERIMENT="$(GOEXPERIMENT)"
+COMMON_ARGS += --build-arg=PROTOBUF_GO_VERSION="$(PROTOBUF_GO_VERSION)"
+COMMON_ARGS += --build-arg=GRPC_GO_VERSION="$(GRPC_GO_VERSION)"
+COMMON_ARGS += --build-arg=GRPC_GATEWAY_VERSION="$(GRPC_GATEWAY_VERSION)"
+COMMON_ARGS += --build-arg=VTPROTOBUF_VERSION="$(VTPROTOBUF_VERSION)"
+COMMON_ARGS += --build-arg=GOIMPORTS_VERSION="$(GOIMPORTS_VERSION)"
+COMMON_ARGS += --build-arg=DEEPCOPY_VERSION="$(DEEPCOPY_VERSION)"
+COMMON_ARGS += --build-arg=GOLANGCILINT_VERSION="$(GOLANGCILINT_VERSION)"
+COMMON_ARGS += --build-arg=GOFUMPT_VERSION="$(GOFUMPT_VERSION)"
+COMMON_ARGS += --build-arg=TESTPKGS="$(TESTPKGS)"
+TOOLCHAIN ?= docker.io/golang:1.23-alpine
+
+# help menu
+
+export define HELP_MENU_HEADER
+# Getting Started
+
+To build this project, you must have the following installed:
+
+- git
+- make
+- docker (19.03 or higher)
+
+## Creating a Builder Instance
+
+The build process makes use of experimental Docker features (buildx).
+To enable experimental features, add 'experimental: "true"' to '/etc/docker/daemon.json' on
+Linux or enable experimental features in Docker GUI for Windows or Mac.
+
+To create a builder instance, run:
+
+	docker buildx create --name local --use
+
+If running builds that needs to be cached aggresively create a builder instance with the following:
+
+	docker buildx create --name local --use --config=config.toml
+
+config.toml contents:
+
+[worker.oci]
+  gc = true
+  gckeepstorage = 50000
+
+  [[worker.oci.gcpolicy]]
+    keepBytes = 10737418240
+    keepDuration = 604800
+    filters = [ "type==source.local", "type==exec.cachemount", "type==source.git.checkout"]
+  [[worker.oci.gcpolicy]]
+    all = true
+    keepBytes = 53687091200
+
+If you already have a compatible builder instance, you may use that instead.
+
+## Artifacts
+
+All artifacts will be output to ./$(ARTIFACTS). Images will be tagged with the
+registry "$(REGISTRY)", username "$(USERNAME)", and a dynamic tag (e.g. $(IMAGE):$(IMAGE_TAG)).
+The registry and username can be overridden by exporting REGISTRY, and USERNAME
+respectively.
+
+endef
+
+ifneq (, $(filter $(WITH_RACE), t true TRUE y yes 1))
+GO_BUILDFLAGS += -race
+CGO_ENABLED := 1
+GO_LDFLAGS += -linkmode=external -extldflags '-static'
+endif
+
+ifneq (, $(filter $(WITH_DEBUG), t true TRUE y yes 1))
+GO_BUILDFLAGS += -tags sidero.debug
+else
+GO_LDFLAGS += -s
+endif
+
+all: unit-tests provider image-provider ipxe lint
+
+$(ARTIFACTS):  ## Creates artifacts directory.
+	@mkdir -p $(ARTIFACTS)
+
+.PHONY: clean
+clean:  ## Cleans up all artifacts.
+	@rm -rf $(ARTIFACTS)
+
+target-%:  ## Builds the specified target defined in the Dockerfile. The build result will only remain in the build cache.
+	@$(BUILD) --target=$* $(COMMON_ARGS) $(TARGET_ARGS) $(CI_ARGS) .
+
+local-%:  ## Builds the specified target defined in the Dockerfile using the local output type. The build result will be output to the specified local destination.
+	@$(MAKE) target-$* TARGET_ARGS="--output=type=local,dest=$(DEST) $(TARGET_ARGS)"
+
+generate:  ## Generate .proto definitions.
+	@$(MAKE) local-$@ DEST=./
+
+lint-golangci-lint:  ## Runs golangci-lint linter.
+	@$(MAKE) target-$@
+
+lint-gofumpt:  ## Runs gofumpt linter.
+	@$(MAKE) target-$@
+
+.PHONY: fmt
+fmt:  ## Formats the source code
+	@docker run --rm -it -v $(PWD):/src -w /src golang:$(GO_VERSION) \
+		bash -c "export GOTOOLCHAIN=local; \
+		export GO111MODULE=on; export GOPROXY=https://proxy.golang.org; \
+		go install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) && \
+		gofumpt -w ."
+
+lint-govulncheck:  ## Runs govulncheck linter.
+	@$(MAKE) target-$@
+
+.PHONY: base
+base:  ## Prepare base toolchain
+	@$(MAKE) target-$@
+
+.PHONY: unit-tests
+unit-tests:  ## Performs unit tests
+	@$(MAKE) local-$@ DEST=$(ARTIFACTS)
+
+.PHONY: unit-tests-race
+unit-tests-race:  ## Performs unit tests with race detection enabled.
+	@$(MAKE) target-$@
+
+.PHONY: $(ARTIFACTS)/provider-linux-amd64
+$(ARTIFACTS)/provider-linux-amd64:
+	@$(MAKE) local-provider-linux-amd64 DEST=$(ARTIFACTS)
+
+.PHONY: provider-linux-amd64
+provider-linux-amd64: $(ARTIFACTS)/provider-linux-amd64  ## Builds executable for provider-linux-amd64.
+
+.PHONY: $(ARTIFACTS)/provider-linux-arm64
+$(ARTIFACTS)/provider-linux-arm64:
+	@$(MAKE) local-provider-linux-arm64 DEST=$(ARTIFACTS)
+
+.PHONY: provider-linux-arm64
+provider-linux-arm64: $(ARTIFACTS)/provider-linux-arm64  ## Builds executable for provider-linux-arm64.
+
+.PHONY: provider
+provider: provider-linux-amd64 provider-linux-arm64  ## Builds executables for provider.
+
+.PHONY: lint-markdown
+lint-markdown:  ## Runs markdownlint.
+	@$(MAKE) target-$@
+
+.PHONY: lint
+lint: lint-golangci-lint lint-gofumpt lint-govulncheck lint-markdown  ## Run all linters for the project.
+
+.PHONY: image-provider
+image-provider:  ## Builds image for provider.
+	@$(MAKE) target-$@ TARGET_ARGS="--tag=$(REGISTRY)/$(USERNAME)/provider:$(IMAGE_TAG)"
+
+.PHONY: rekres
+rekres:
+	@docker pull $(KRES_IMAGE)
+	@docker run --rm --net=host --user $(shell id -u):$(shell id -g) -v $(PWD):/src -w /src -e GITHUB_TOKEN $(KRES_IMAGE)
+
+.PHONY: help
+help:  ## This help menu.
+	@echo "$$HELP_MENU_HEADER"
+	@grep -E '^[a-zA-Z%_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
+
+.PHONY: release-notes
+release-notes: $(ARTIFACTS)
+	@ARTIFACTS=$(ARTIFACTS) ./hack/release.sh $@ $(ARTIFACTS)/RELEASE_NOTES.md $(TAG)
+
+.PHONY: conformance
+conformance:
+	@docker pull $(CONFORMANCE_IMAGE)
+	@docker run --rm -it -v $(PWD):/src -w /src $(CONFORMANCE_IMAGE) enforce
+
diff --git a/README.md b/README.md
index 8586b13..9734a81 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,6 @@
 # omni-infra-provider-bare-metal
+
+This repo contains the code of the following:
+
+- Omni bare metal infra provider
+- Talos metal agent service
diff --git a/api/provider/provider.pb.go b/api/provider/provider.pb.go
new file mode 100644
index 0000000..280addc
--- /dev/null
+++ b/api/provider/provider.pb.go
@@ -0,0 +1,187 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.35.1
+// 	protoc        v4.24.4
+// source: provider/provider.proto
+
+package providerpb
+
+import (
+	reflect "reflect"
+	sync "sync"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	_ "google.golang.org/protobuf/types/known/durationpb"
+	_ "google.golang.org/protobuf/types/known/emptypb"
+	_ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+type RebootMachineRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+}
+
+func (x *RebootMachineRequest) Reset() {
+	*x = RebootMachineRequest{}
+	mi := &file_provider_provider_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RebootMachineRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RebootMachineRequest) ProtoMessage() {}
+
+func (x *RebootMachineRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_provider_provider_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RebootMachineRequest.ProtoReflect.Descriptor instead.
+func (*RebootMachineRequest) Descriptor() ([]byte, []int) {
+	return file_provider_provider_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *RebootMachineRequest) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+type RebootMachineResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+}
+
+func (x *RebootMachineResponse) Reset() {
+	*x = RebootMachineResponse{}
+	mi := &file_provider_provider_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *RebootMachineResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RebootMachineResponse) ProtoMessage() {}
+
+func (x *RebootMachineResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_provider_provider_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use RebootMachineResponse.ProtoReflect.Descriptor instead.
+func (*RebootMachineResponse) Descriptor() ([]byte, []int) {
+	return file_provider_provider_proto_rawDescGZIP(), []int{1}
+}
+
+var File_provider_provider_proto protoreflect.FileDescriptor
+
+var file_provider_provider_proto_rawDesc = []byte{
+	0x0a, 0x17, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69,
+	0x64, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x22, 0x26, 0x0a, 0x14, 0x52, 0x65, 0x62, 0x6f, 0x6f, 0x74, 0x4d, 0x61, 0x63,
+	0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69,
+	0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x17, 0x0a, 0x15, 0x52,
+	0x65, 0x62, 0x6f, 0x6f, 0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x32, 0x67, 0x0a, 0x0f, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
+	0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x54, 0x0a, 0x0d, 0x52, 0x65, 0x62, 0x6f, 0x6f,
+	0x74, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x12, 0x20, 0x2e, 0x6d, 0x61, 0x6e, 0x61, 0x67,
+	0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x62, 0x6f, 0x6f, 0x74, 0x4d, 0x61, 0x63, 0x68,
+	0x69, 0x6e, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x6d, 0x61, 0x6e,
+	0x61, 0x67, 0x65, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x62, 0x6f, 0x6f, 0x74, 0x4d, 0x61,
+	0x63, 0x68, 0x69, 0x6e, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3d, 0x5a,
+	0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65,
+	0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x6c, 0x6f, 0x73, 0x2d, 0x6d, 0x65, 0x74,
+	0x61, 0x6c, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61,
+	0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_provider_provider_proto_rawDescOnce sync.Once
+	file_provider_provider_proto_rawDescData = file_provider_provider_proto_rawDesc
+)
+
+func file_provider_provider_proto_rawDescGZIP() []byte {
+	file_provider_provider_proto_rawDescOnce.Do(func() {
+		file_provider_provider_proto_rawDescData = protoimpl.X.CompressGZIP(file_provider_provider_proto_rawDescData)
+	})
+	return file_provider_provider_proto_rawDescData
+}
+
+var file_provider_provider_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
+var file_provider_provider_proto_goTypes = []any{
+	(*RebootMachineRequest)(nil),  // 0: management.RebootMachineRequest
+	(*RebootMachineResponse)(nil), // 1: management.RebootMachineResponse
+}
+var file_provider_provider_proto_depIdxs = []int32{
+	0, // 0: management.ProviderService.RebootMachine:input_type -> management.RebootMachineRequest
+	1, // 1: management.ProviderService.RebootMachine:output_type -> management.RebootMachineResponse
+	1, // [1:2] is the sub-list for method output_type
+	0, // [0:1] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_provider_provider_proto_init() }
+func file_provider_provider_proto_init() {
+	if File_provider_provider_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_provider_provider_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   2,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_provider_provider_proto_goTypes,
+		DependencyIndexes: file_provider_provider_proto_depIdxs,
+		MessageInfos:      file_provider_provider_proto_msgTypes,
+	}.Build()
+	File_provider_provider_proto = out.File
+	file_provider_provider_proto_rawDesc = nil
+	file_provider_provider_proto_goTypes = nil
+	file_provider_provider_proto_depIdxs = nil
+}
diff --git a/api/provider/provider.proto b/api/provider/provider.proto
new file mode 100644
index 0000000..037585e
--- /dev/null
+++ b/api/provider/provider.proto
@@ -0,0 +1,18 @@
+syntax = "proto3";
+package management;
+
+option go_package = "github.com/siderolabs/talos-metal-agent/internal/providerpb";
+
+import "google/protobuf/empty.proto";
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+message RebootMachineRequest {
+  string id = 1;
+}
+
+message RebootMachineResponse {}
+
+service ProviderService {
+  rpc RebootMachine(RebootMachineRequest) returns (RebootMachineResponse);
+}
diff --git a/api/provider/provider_grpc.pb.go b/api/provider/provider_grpc.pb.go
new file mode 100644
index 0000000..f935750
--- /dev/null
+++ b/api/provider/provider_grpc.pb.go
@@ -0,0 +1,122 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc             v4.24.4
+// source: provider/provider.proto
+
+package providerpb
+
+import (
+	context "context"
+
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+	ProviderService_RebootMachine_FullMethodName = "/management.ProviderService/RebootMachine"
+)
+
+// ProviderServiceClient is the client API for ProviderService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type ProviderServiceClient interface {
+	RebootMachine(ctx context.Context, in *RebootMachineRequest, opts ...grpc.CallOption) (*RebootMachineResponse, error)
+}
+
+type providerServiceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewProviderServiceClient(cc grpc.ClientConnInterface) ProviderServiceClient {
+	return &providerServiceClient{cc}
+}
+
+func (c *providerServiceClient) RebootMachine(ctx context.Context, in *RebootMachineRequest, opts ...grpc.CallOption) (*RebootMachineResponse, error) {
+	cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+	out := new(RebootMachineResponse)
+	err := c.cc.Invoke(ctx, ProviderService_RebootMachine_FullMethodName, in, out, cOpts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// ProviderServiceServer is the server API for ProviderService service.
+// All implementations must embed UnimplementedProviderServiceServer
+// for forward compatibility.
+type ProviderServiceServer interface {
+	RebootMachine(context.Context, *RebootMachineRequest) (*RebootMachineResponse, error)
+	mustEmbedUnimplementedProviderServiceServer()
+}
+
+// UnimplementedProviderServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedProviderServiceServer struct{}
+
+func (UnimplementedProviderServiceServer) RebootMachine(context.Context, *RebootMachineRequest) (*RebootMachineResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method RebootMachine not implemented")
+}
+func (UnimplementedProviderServiceServer) mustEmbedUnimplementedProviderServiceServer() {}
+func (UnimplementedProviderServiceServer) testEmbeddedByValue()                         {}
+
+// UnsafeProviderServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to ProviderServiceServer will
+// result in compilation errors.
+type UnsafeProviderServiceServer interface {
+	mustEmbedUnimplementedProviderServiceServer()
+}
+
+func RegisterProviderServiceServer(s grpc.ServiceRegistrar, srv ProviderServiceServer) {
+	// If the following call pancis, it indicates UnimplementedProviderServiceServer was
+	// embedded by pointer and is nil.  This will cause panics if an
+	// unimplemented method is ever invoked, so we test this at initialization
+	// time to prevent it from happening at runtime later due to I/O.
+	if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+		t.testEmbeddedByValue()
+	}
+	s.RegisterService(&ProviderService_ServiceDesc, srv)
+}
+
+func _ProviderService_RebootMachine_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(RebootMachineRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(ProviderServiceServer).RebootMachine(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: ProviderService_RebootMachine_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(ProviderServiceServer).RebootMachine(ctx, req.(*RebootMachineRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// ProviderService_ServiceDesc is the grpc.ServiceDesc for ProviderService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var ProviderService_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "management.ProviderService",
+	HandlerType: (*ProviderServiceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "RebootMachine",
+			Handler:    _ProviderService_RebootMachine_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "provider/provider.proto",
+}
diff --git a/api/provider/provider_vtproto.pb.go b/api/provider/provider_vtproto.pb.go
new file mode 100644
index 0000000..c057c50
--- /dev/null
+++ b/api/provider/provider_vtproto.pb.go
@@ -0,0 +1,321 @@
+// Code generated by protoc-gen-go-vtproto. DO NOT EDIT.
+// protoc-gen-go-vtproto version: v0.6.0
+// source: provider/provider.proto
+
+package providerpb
+
+import (
+	fmt "fmt"
+	io "io"
+
+	protohelpers "github.com/planetscale/vtprotobuf/protohelpers"
+	proto "google.golang.org/protobuf/proto"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+func (m *RebootMachineRequest) CloneVT() *RebootMachineRequest {
+	if m == nil {
+		return (*RebootMachineRequest)(nil)
+	}
+	r := new(RebootMachineRequest)
+	r.Id = m.Id
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *RebootMachineRequest) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
+func (m *RebootMachineResponse) CloneVT() *RebootMachineResponse {
+	if m == nil {
+		return (*RebootMachineResponse)(nil)
+	}
+	r := new(RebootMachineResponse)
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *RebootMachineResponse) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
+func (this *RebootMachineRequest) EqualVT(that *RebootMachineRequest) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	if this.Id != that.Id {
+		return false
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *RebootMachineRequest) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*RebootMachineRequest)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
+func (this *RebootMachineResponse) EqualVT(that *RebootMachineResponse) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *RebootMachineResponse) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*RebootMachineResponse)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
+func (m *RebootMachineRequest) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *RebootMachineRequest) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *RebootMachineRequest) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	if len(m.Id) > 0 {
+		i -= len(m.Id)
+		copy(dAtA[i:], m.Id)
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Id)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *RebootMachineResponse) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *RebootMachineResponse) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *RebootMachineResponse) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *RebootMachineRequest) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.Id)
+	if l > 0 {
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	n += len(m.unknownFields)
+	return n
+}
+
+func (m *RebootMachineResponse) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	n += len(m.unknownFields)
+	return n
+}
+
+func (m *RebootMachineRequest) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: RebootMachineRequest: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: RebootMachineRequest: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Id", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Id = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *RebootMachineResponse) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: RebootMachineResponse: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: RebootMachineResponse: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
diff --git a/api/specs/specs.pb.go b/api/specs/specs.pb.go
new file mode 100644
index 0000000..1d255ed
--- /dev/null
+++ b/api/specs/specs.pb.go
@@ -0,0 +1,267 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.35.1
+// 	protoc        v4.24.4
+// source: specs/specs.proto
+
+package specs
+
+import (
+	reflect "reflect"
+	sync "sync"
+
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	_ "google.golang.org/protobuf/types/known/durationpb"
+	_ "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// MachineSpec is stored in Omni in the infra provisioner state.
+type MachineSpec struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ipmi *MachineSpec_IPMIInfo `protobuf:"bytes,1,opt,name=ipmi,proto3" json:"ipmi,omitempty"`
+	Api  *MachineSpec_APIInfo  `protobuf:"bytes,2,opt,name=api,proto3" json:"api,omitempty"`
+}
+
+func (x *MachineSpec) Reset() {
+	*x = MachineSpec{}
+	mi := &file_specs_specs_proto_msgTypes[0]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *MachineSpec) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MachineSpec) ProtoMessage() {}
+
+func (x *MachineSpec) ProtoReflect() protoreflect.Message {
+	mi := &file_specs_specs_proto_msgTypes[0]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MachineSpec.ProtoReflect.Descriptor instead.
+func (*MachineSpec) Descriptor() ([]byte, []int) {
+	return file_specs_specs_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *MachineSpec) GetIpmi() *MachineSpec_IPMIInfo {
+	if x != nil {
+		return x.Ipmi
+	}
+	return nil
+}
+
+func (x *MachineSpec) GetApi() *MachineSpec_APIInfo {
+	if x != nil {
+		return x.Api
+	}
+	return nil
+}
+
+type MachineSpec_IPMIInfo struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Ip       string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
+	Port     uint32 `protobuf:"varint,2,opt,name=port,proto3" json:"port,omitempty"`
+	Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
+}
+
+func (x *MachineSpec_IPMIInfo) Reset() {
+	*x = MachineSpec_IPMIInfo{}
+	mi := &file_specs_specs_proto_msgTypes[1]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *MachineSpec_IPMIInfo) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MachineSpec_IPMIInfo) ProtoMessage() {}
+
+func (x *MachineSpec_IPMIInfo) ProtoReflect() protoreflect.Message {
+	mi := &file_specs_specs_proto_msgTypes[1]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MachineSpec_IPMIInfo.ProtoReflect.Descriptor instead.
+func (*MachineSpec_IPMIInfo) Descriptor() ([]byte, []int) {
+	return file_specs_specs_proto_rawDescGZIP(), []int{0, 0}
+}
+
+func (x *MachineSpec_IPMIInfo) GetIp() string {
+	if x != nil {
+		return x.Ip
+	}
+	return ""
+}
+
+func (x *MachineSpec_IPMIInfo) GetPort() uint32 {
+	if x != nil {
+		return x.Port
+	}
+	return 0
+}
+
+func (x *MachineSpec_IPMIInfo) GetPassword() string {
+	if x != nil {
+		return x.Password
+	}
+	return ""
+}
+
+type MachineSpec_APIInfo struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Address string `protobuf:"bytes,1,opt,name=address,proto3" json:"address,omitempty"`
+}
+
+func (x *MachineSpec_APIInfo) Reset() {
+	*x = MachineSpec_APIInfo{}
+	mi := &file_specs_specs_proto_msgTypes[2]
+	ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+	ms.StoreMessageInfo(mi)
+}
+
+func (x *MachineSpec_APIInfo) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MachineSpec_APIInfo) ProtoMessage() {}
+
+func (x *MachineSpec_APIInfo) ProtoReflect() protoreflect.Message {
+	mi := &file_specs_specs_proto_msgTypes[2]
+	if x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MachineSpec_APIInfo.ProtoReflect.Descriptor instead.
+func (*MachineSpec_APIInfo) Descriptor() ([]byte, []int) {
+	return file_specs_specs_proto_rawDescGZIP(), []int{0, 1}
+}
+
+func (x *MachineSpec_APIInfo) GetAddress() string {
+	if x != nil {
+		return x.Address
+	}
+	return ""
+}
+
+var File_specs_specs_proto protoreflect.FileDescriptor
+
+var file_specs_specs_proto_rawDesc = []byte{
+	0x0a, 0x11, 0x73, 0x70, 0x65, 0x63, 0x73, 0x2f, 0x73, 0x70, 0x65, 0x63, 0x73, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x12, 0x08, 0x65, 0x6d, 0x75, 0x73, 0x70, 0x65, 0x63, 0x73, 0x1a, 0x1f, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74,
+	0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f,
+	0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe3,
+	0x01, 0x0a, 0x0b, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x32,
+	0x0a, 0x04, 0x69, 0x70, 0x6d, 0x69, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x65,
+	0x6d, 0x75, 0x73, 0x70, 0x65, 0x63, 0x73, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69, 0x6e, 0x65, 0x53,
+	0x70, 0x65, 0x63, 0x2e, 0x49, 0x50, 0x4d, 0x49, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x04, 0x69, 0x70,
+	0x6d, 0x69, 0x12, 0x2f, 0x0a, 0x03, 0x61, 0x70, 0x69, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+	0x1d, 0x2e, 0x65, 0x6d, 0x75, 0x73, 0x70, 0x65, 0x63, 0x73, 0x2e, 0x4d, 0x61, 0x63, 0x68, 0x69,
+	0x6e, 0x65, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x41, 0x50, 0x49, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x03,
+	0x61, 0x70, 0x69, 0x1a, 0x4a, 0x0a, 0x08, 0x49, 0x50, 0x4d, 0x49, 0x49, 0x6e, 0x66, 0x6f, 0x12,
+	0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12,
+	0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x70,
+	0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18,
+	0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x1a,
+	0x23, 0x0a, 0x07, 0x41, 0x50, 0x49, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64,
+	0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64,
+	0x72, 0x65, 0x73, 0x73, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2f, 0x73, 0x69, 0x64, 0x65, 0x72, 0x6f, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x6f, 0x6d,
+	0x6e, 0x69, 0x2d, 0x69, 0x6e, 0x66, 0x72, 0x61, 0x2d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65,
+	0x72, 0x2d, 0x6b, 0x75, 0x62, 0x65, 0x76, 0x69, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73,
+	0x70, 0x65, 0x63, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_specs_specs_proto_rawDescOnce sync.Once
+	file_specs_specs_proto_rawDescData = file_specs_specs_proto_rawDesc
+)
+
+func file_specs_specs_proto_rawDescGZIP() []byte {
+	file_specs_specs_proto_rawDescOnce.Do(func() {
+		file_specs_specs_proto_rawDescData = protoimpl.X.CompressGZIP(file_specs_specs_proto_rawDescData)
+	})
+	return file_specs_specs_proto_rawDescData
+}
+
+var file_specs_specs_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
+var file_specs_specs_proto_goTypes = []any{
+	(*MachineSpec)(nil),          // 0: emuspecs.MachineSpec
+	(*MachineSpec_IPMIInfo)(nil), // 1: emuspecs.MachineSpec.IPMIInfo
+	(*MachineSpec_APIInfo)(nil),  // 2: emuspecs.MachineSpec.APIInfo
+}
+var file_specs_specs_proto_depIdxs = []int32{
+	1, // 0: emuspecs.MachineSpec.ipmi:type_name -> emuspecs.MachineSpec.IPMIInfo
+	2, // 1: emuspecs.MachineSpec.api:type_name -> emuspecs.MachineSpec.APIInfo
+	2, // [2:2] is the sub-list for method output_type
+	2, // [2:2] is the sub-list for method input_type
+	2, // [2:2] is the sub-list for extension type_name
+	2, // [2:2] is the sub-list for extension extendee
+	0, // [0:2] is the sub-list for field type_name
+}
+
+func init() { file_specs_specs_proto_init() }
+func file_specs_specs_proto_init() {
+	if File_specs_specs_proto != nil {
+		return
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_specs_specs_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   3,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_specs_specs_proto_goTypes,
+		DependencyIndexes: file_specs_specs_proto_depIdxs,
+		MessageInfos:      file_specs_specs_proto_msgTypes,
+	}.Build()
+	File_specs_specs_proto = out.File
+	file_specs_specs_proto_rawDesc = nil
+	file_specs_specs_proto_goTypes = nil
+	file_specs_specs_proto_depIdxs = nil
+}
diff --git a/api/specs/specs.proto b/api/specs/specs.proto
new file mode 100644
index 0000000..078515e
--- /dev/null
+++ b/api/specs/specs.proto
@@ -0,0 +1,23 @@
+syntax = "proto3";
+package emuspecs;
+
+option go_package = "github.com/siderolabs/omni-infra-provider-kubevirt/api/specs";
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+
+// MachineSpec is stored in Omni in the infra provisioner state.
+message MachineSpec {
+  message IPMIInfo {
+    string ip = 1;
+    uint32 port = 2;
+    string password = 3;
+  }
+
+  message APIInfo {
+    string address = 1;
+  }
+
+  IPMIInfo ipmi = 1;
+  APIInfo api = 2;
+}
diff --git a/api/specs/specs_vtproto.pb.go b/api/specs/specs_vtproto.pb.go
new file mode 100644
index 0000000..d147532
--- /dev/null
+++ b/api/specs/specs_vtproto.pb.go
@@ -0,0 +1,680 @@
+// Code generated by protoc-gen-go-vtproto. DO NOT EDIT.
+// protoc-gen-go-vtproto version: v0.6.0
+// source: specs/specs.proto
+
+package specs
+
+import (
+	fmt "fmt"
+	io "io"
+
+	protohelpers "github.com/planetscale/vtprotobuf/protohelpers"
+	proto "google.golang.org/protobuf/proto"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+func (m *MachineSpec_IPMIInfo) CloneVT() *MachineSpec_IPMIInfo {
+	if m == nil {
+		return (*MachineSpec_IPMIInfo)(nil)
+	}
+	r := new(MachineSpec_IPMIInfo)
+	r.Ip = m.Ip
+	r.Port = m.Port
+	r.Password = m.Password
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *MachineSpec_IPMIInfo) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
+func (m *MachineSpec_APIInfo) CloneVT() *MachineSpec_APIInfo {
+	if m == nil {
+		return (*MachineSpec_APIInfo)(nil)
+	}
+	r := new(MachineSpec_APIInfo)
+	r.Address = m.Address
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *MachineSpec_APIInfo) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
+func (m *MachineSpec) CloneVT() *MachineSpec {
+	if m == nil {
+		return (*MachineSpec)(nil)
+	}
+	r := new(MachineSpec)
+	r.Ipmi = m.Ipmi.CloneVT()
+	r.Api = m.Api.CloneVT()
+	if len(m.unknownFields) > 0 {
+		r.unknownFields = make([]byte, len(m.unknownFields))
+		copy(r.unknownFields, m.unknownFields)
+	}
+	return r
+}
+
+func (m *MachineSpec) CloneMessageVT() proto.Message {
+	return m.CloneVT()
+}
+
+func (this *MachineSpec_IPMIInfo) EqualVT(that *MachineSpec_IPMIInfo) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	if this.Ip != that.Ip {
+		return false
+	}
+	if this.Port != that.Port {
+		return false
+	}
+	if this.Password != that.Password {
+		return false
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *MachineSpec_IPMIInfo) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*MachineSpec_IPMIInfo)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
+func (this *MachineSpec_APIInfo) EqualVT(that *MachineSpec_APIInfo) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	if this.Address != that.Address {
+		return false
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *MachineSpec_APIInfo) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*MachineSpec_APIInfo)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
+func (this *MachineSpec) EqualVT(that *MachineSpec) bool {
+	if this == that {
+		return true
+	} else if this == nil || that == nil {
+		return false
+	}
+	if !this.Ipmi.EqualVT(that.Ipmi) {
+		return false
+	}
+	if !this.Api.EqualVT(that.Api) {
+		return false
+	}
+	return string(this.unknownFields) == string(that.unknownFields)
+}
+
+func (this *MachineSpec) EqualMessageVT(thatMsg proto.Message) bool {
+	that, ok := thatMsg.(*MachineSpec)
+	if !ok {
+		return false
+	}
+	return this.EqualVT(that)
+}
+func (m *MachineSpec_IPMIInfo) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *MachineSpec_IPMIInfo) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *MachineSpec_IPMIInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	if len(m.Password) > 0 {
+		i -= len(m.Password)
+		copy(dAtA[i:], m.Password)
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Password)))
+		i--
+		dAtA[i] = 0x1a
+	}
+	if m.Port != 0 {
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(m.Port))
+		i--
+		dAtA[i] = 0x10
+	}
+	if len(m.Ip) > 0 {
+		i -= len(m.Ip)
+		copy(dAtA[i:], m.Ip)
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Ip)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *MachineSpec_APIInfo) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *MachineSpec_APIInfo) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *MachineSpec_APIInfo) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	if len(m.Address) > 0 {
+		i -= len(m.Address)
+		copy(dAtA[i:], m.Address)
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(len(m.Address)))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *MachineSpec) MarshalVT() (dAtA []byte, err error) {
+	if m == nil {
+		return nil, nil
+	}
+	size := m.SizeVT()
+	dAtA = make([]byte, size)
+	n, err := m.MarshalToSizedBufferVT(dAtA[:size])
+	if err != nil {
+		return nil, err
+	}
+	return dAtA[:n], nil
+}
+
+func (m *MachineSpec) MarshalToVT(dAtA []byte) (int, error) {
+	size := m.SizeVT()
+	return m.MarshalToSizedBufferVT(dAtA[:size])
+}
+
+func (m *MachineSpec) MarshalToSizedBufferVT(dAtA []byte) (int, error) {
+	if m == nil {
+		return 0, nil
+	}
+	i := len(dAtA)
+	_ = i
+	var l int
+	_ = l
+	if m.unknownFields != nil {
+		i -= len(m.unknownFields)
+		copy(dAtA[i:], m.unknownFields)
+	}
+	if m.Api != nil {
+		size, err := m.Api.MarshalToSizedBufferVT(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
+		i--
+		dAtA[i] = 0x12
+	}
+	if m.Ipmi != nil {
+		size, err := m.Ipmi.MarshalToSizedBufferVT(dAtA[:i])
+		if err != nil {
+			return 0, err
+		}
+		i -= size
+		i = protohelpers.EncodeVarint(dAtA, i, uint64(size))
+		i--
+		dAtA[i] = 0xa
+	}
+	return len(dAtA) - i, nil
+}
+
+func (m *MachineSpec_IPMIInfo) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.Ip)
+	if l > 0 {
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	if m.Port != 0 {
+		n += 1 + protohelpers.SizeOfVarint(uint64(m.Port))
+	}
+	l = len(m.Password)
+	if l > 0 {
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	n += len(m.unknownFields)
+	return n
+}
+
+func (m *MachineSpec_APIInfo) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	l = len(m.Address)
+	if l > 0 {
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	n += len(m.unknownFields)
+	return n
+}
+
+func (m *MachineSpec) SizeVT() (n int) {
+	if m == nil {
+		return 0
+	}
+	var l int
+	_ = l
+	if m.Ipmi != nil {
+		l = m.Ipmi.SizeVT()
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	if m.Api != nil {
+		l = m.Api.SizeVT()
+		n += 1 + l + protohelpers.SizeOfVarint(uint64(l))
+	}
+	n += len(m.unknownFields)
+	return n
+}
+
+func (m *MachineSpec_IPMIInfo) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: MachineSpec_IPMIInfo: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: MachineSpec_IPMIInfo: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Ip", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Ip = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		case 2:
+			if wireType != 0 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Port", wireType)
+			}
+			m.Port = 0
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				m.Port |= uint32(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+		case 3:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Password", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Password = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *MachineSpec_APIInfo) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: MachineSpec_APIInfo: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: MachineSpec_APIInfo: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Address", wireType)
+			}
+			var stringLen uint64
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				stringLen |= uint64(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			intStringLen := int(stringLen)
+			if intStringLen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + intStringLen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.Address = string(dAtA[iNdEx:postIndex])
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
+func (m *MachineSpec) UnmarshalVT(dAtA []byte) error {
+	l := len(dAtA)
+	iNdEx := 0
+	for iNdEx < l {
+		preIndex := iNdEx
+		var wire uint64
+		for shift := uint(0); ; shift += 7 {
+			if shift >= 64 {
+				return protohelpers.ErrIntOverflow
+			}
+			if iNdEx >= l {
+				return io.ErrUnexpectedEOF
+			}
+			b := dAtA[iNdEx]
+			iNdEx++
+			wire |= uint64(b&0x7F) << shift
+			if b < 0x80 {
+				break
+			}
+		}
+		fieldNum := int32(wire >> 3)
+		wireType := int(wire & 0x7)
+		if wireType == 4 {
+			return fmt.Errorf("proto: MachineSpec: wiretype end group for non-group")
+		}
+		if fieldNum <= 0 {
+			return fmt.Errorf("proto: MachineSpec: illegal tag %d (wire type %d)", fieldNum, wire)
+		}
+		switch fieldNum {
+		case 1:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Ipmi", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Ipmi == nil {
+				m.Ipmi = &MachineSpec_IPMIInfo{}
+			}
+			if err := m.Ipmi.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		case 2:
+			if wireType != 2 {
+				return fmt.Errorf("proto: wrong wireType = %d for field Api", wireType)
+			}
+			var msglen int
+			for shift := uint(0); ; shift += 7 {
+				if shift >= 64 {
+					return protohelpers.ErrIntOverflow
+				}
+				if iNdEx >= l {
+					return io.ErrUnexpectedEOF
+				}
+				b := dAtA[iNdEx]
+				iNdEx++
+				msglen |= int(b&0x7F) << shift
+				if b < 0x80 {
+					break
+				}
+			}
+			if msglen < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			postIndex := iNdEx + msglen
+			if postIndex < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if postIndex > l {
+				return io.ErrUnexpectedEOF
+			}
+			if m.Api == nil {
+				m.Api = &MachineSpec_APIInfo{}
+			}
+			if err := m.Api.UnmarshalVT(dAtA[iNdEx:postIndex]); err != nil {
+				return err
+			}
+			iNdEx = postIndex
+		default:
+			iNdEx = preIndex
+			skippy, err := protohelpers.Skip(dAtA[iNdEx:])
+			if err != nil {
+				return err
+			}
+			if (skippy < 0) || (iNdEx+skippy) < 0 {
+				return protohelpers.ErrInvalidLength
+			}
+			if (iNdEx + skippy) > l {
+				return io.ErrUnexpectedEOF
+			}
+			m.unknownFields = append(m.unknownFields, dAtA[iNdEx:iNdEx+skippy]...)
+			iNdEx += skippy
+		}
+	}
+
+	if iNdEx > l {
+		return io.ErrUnexpectedEOF
+	}
+	return nil
+}
diff --git a/cmd/provider/main.go b/cmd/provider/main.go
new file mode 100644
index 0000000..51377b0
--- /dev/null
+++ b/cmd/provider/main.go
@@ -0,0 +1,167 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package main implements the main entrypoint for the Omni bare metal infra provider.
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/spf13/cobra"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/ip"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/meta"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/provider"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/version"
+)
+
+const apiAdvertiseAddressFlag = "api-advertise-address"
+
+var rootCmdArgs struct {
+	apiListenAddress         string
+	apiAdvertiseAddress      string
+	omniAPIEndpoint          string
+	imageFactoryPXEURL       string
+	providerName             string
+	providerDescription      string
+	agentTalosImage          string
+	imageFactoryBaseURL      string
+	imageFactoryPXEBaseURL   string
+	imageFactoryTalosVersion string
+	apiPowerMgmtStateDir     string
+	machineLabels            []string
+	apiPort                  int
+	insecureSkipTLSVerify    bool
+	debug                    bool
+}
+
+// rootCmd represents the base command when called without any subcommands.
+var rootCmd = &cobra.Command{
+	Use:     version.Name,
+	Short:   "Run the Omni bare metal infra provider",
+	Version: version.Tag,
+	Args:    cobra.NoArgs,
+	PersistentPreRun: func(cmd *cobra.Command, _ []string) {
+		cmd.SilenceUsage = true // if the args are parsed fine, no need to show usage
+	},
+	RunE: func(cmd *cobra.Command, _ []string) error {
+		logger, err := initLogger()
+		if err != nil {
+			return fmt.Errorf("failed to create logger: %w", err)
+		}
+
+		defer logger.Sync() //nolint:errcheck
+
+		return run(cmd.Context(), logger)
+	},
+}
+
+func initLogger() (*zap.Logger, error) {
+	var loggerConfig zap.Config
+
+	if rootCmdArgs.debug {
+		loggerConfig = zap.NewDevelopmentConfig()
+		loggerConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
+		loggerConfig.Level.SetLevel(zap.DebugLevel)
+	} else {
+		loggerConfig = zap.NewProductionConfig()
+		loggerConfig.Level.SetLevel(zap.InfoLevel)
+	}
+
+	return loggerConfig.Build()
+}
+
+func run(ctx context.Context, logger *zap.Logger) error {
+	apiAdvertiseAddress := rootCmdArgs.apiAdvertiseAddress
+
+	if apiAdvertiseAddress == "" {
+		routableIPs, err := ip.RoutableIPs()
+		if err != nil {
+			return fmt.Errorf("failed to get routable IPs: %w", err)
+		}
+
+		if len(routableIPs) != 1 {
+			return fmt.Errorf(`expected exactly one routable IP, got %d: %v. specify "--%s" flag explicitly`, len(routableIPs), routableIPs, apiAdvertiseAddressFlag)
+		}
+
+		apiAdvertiseAddress = routableIPs[0]
+	}
+
+	logger.Info("starting provider", zap.String("api_host", apiAdvertiseAddress), zap.Int("api_port", rootCmdArgs.apiPort))
+
+	options := provider.Options{
+		Name:                     rootCmdArgs.providerName,
+		Description:              rootCmdArgs.providerDescription,
+		OmniAPIEndpoint:          rootCmdArgs.omniAPIEndpoint,
+		APIListenAddress:         rootCmdArgs.apiListenAddress,
+		APIAdvertiseAddress:      rootCmdArgs.apiAdvertiseAddress,
+		APIPort:                  rootCmdArgs.apiPort,
+		AgentTalosImage:          rootCmdArgs.agentTalosImage,
+		ImageFactoryBaseURL:      rootCmdArgs.imageFactoryBaseURL,
+		ImageFactoryPXEBaseURL:   rootCmdArgs.imageFactoryPXEBaseURL,
+		ImageFactoryTalosVersion: rootCmdArgs.imageFactoryTalosVersion,
+		MachineLabels:            rootCmdArgs.machineLabels,
+		InsecureSkipTLSVerify:    rootCmdArgs.insecureSkipTLSVerify,
+		APIPowerMgmtStateDir:     rootCmdArgs.apiPowerMgmtStateDir, // todo: use this
+	}
+
+	prov := provider.New(options, logger)
+
+	if err := prov.Run(ctx); err != nil {
+		return fmt.Errorf("failed to run provider: %w", err)
+	}
+
+	return nil
+}
+
+func main() {
+	if err := runCmd(); err != nil {
+		log.Fatalf("failed to run: %v", err)
+	}
+}
+
+func runCmd() error {
+	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGTERM, os.Interrupt)
+	defer cancel()
+
+	return rootCmd.ExecuteContext(ctx)
+}
+
+func init() {
+	rootCmd.Flags().StringVar(&rootCmdArgs.apiListenAddress, "api-listen-address", "", "The IP address to listen on. If not specified, the server will listen on all interfaces.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.apiAdvertiseAddress, apiAdvertiseAddressFlag, "",
+		"The IP address to advertise. Required if the server has more than a single routable IP address. If not specified, the single routable IP address will be used.")
+
+	rootCmd.Flags().IntVar(&rootCmdArgs.apiPort, "api-port", 50042, "The port to run the api server on.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.omniAPIEndpoint, "omni-api-endpoint", os.Getenv("OMNI_ENDPOINT"),
+		"The endpoint of the Omni API, if not set, defaults to OMNI_ENDPOINT env var.")
+	rootCmd.Flags().StringVar(&meta.ProviderID, "id", meta.ProviderID, "The id of the infra provider, it is used to match the resources with the infra provider label.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.imageFactoryPXEURL, "image-factory-pxe-url", "https://pxe.factory.talos.dev", "The URL of the image factory PXE server.")
+
+	rootCmd.Flags().StringVar(&rootCmdArgs.providerName, "provider-name", "Bare Metal", "Provider name as it appears in Omni")
+	rootCmd.Flags().StringVar(&rootCmdArgs.providerDescription, "provider-description", "Bare metal infrastructure provider", "Provider description as it appears in Omni")
+
+	rootCmd.Flags().StringVar(&rootCmdArgs.agentTalosImage, "agent-talos-image", "", "The Talos metal agent mode image mainly to be used for debugging purposes. If specified, "+
+		"the iPXE server will use the kernel and initramfs from this image instead of forwarding the request to the image factory to boot into agent mode.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.imageFactoryBaseURL, "image-factory-base-url", "https://factory.talos.dev", "The base URL of the image factory.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.imageFactoryPXEBaseURL, "image-factory-pxe-base-url", "https://pxe.factory.talos.dev", "The base URL of the image factory PXE server.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.imageFactoryTalosVersion, "image-factory-talos-version", "v1.8.1",
+		"The Talos version to when forwarding iPXE requests to the image factory.")
+	rootCmd.Flags().StringVar(&rootCmdArgs.apiPowerMgmtStateDir, "api-power-mgmt-state-dir", "",
+		"The directory to read the power management API endpoints and ports, to be used to manage the power state of the machines which are managed via API "+
+			"(e.g., QEMU VMs created by 'talosctl cluster create') Mainly used for testing purposes.")
+
+	rootCmd.Flags().StringSliceVar(&rootCmdArgs.machineLabels, "machine-labels", nil,
+		"Comma separated list of key=value pairs to be set to the machine. Example: key1=value1,key2,key3=value3")
+
+	rootCmd.Flags().BoolVar(&rootCmdArgs.insecureSkipTLSVerify, "insecure-skip-tls-verify", false, "Skip TLS verification when connecting to the Omni API.")
+	rootCmd.Flags().BoolVar(&rootCmdArgs.debug, "debug", false, "Enable debug mode & logs.")
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..9608c4a
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,85 @@
+module github.com/siderolabs/omni-infra-provider-bare-metal
+
+go 1.23.2
+
+replace (
+	github.com/pensando/goipmi v0.0.0-20240603174436-eb122d901c23 => github.com/siderolabs/goipmi v0.0.0-20211214143420-35f956689e67
+	github.com/pin/tftp/v3 v3.1.0 => github.com/utkuozdemir/pin-tftp/v3 v3.0.0-20241021135417-0dd7dba351ad
+	github.com/siderolabs/omni/client v0.0.0-20241017162757-284e8b5077cc => github.com/utkuozdemir/sidero-omni/client v0.0.0-20241025225840-6bccefcfa215
+	github.com/siderolabs/talos-metal-agent v0.0.0-20241016074728-46df49991336 => github.com/utkuozdemir/sidero-talos-metal-agent v0.0.0-20241028223127-f17157c1b44a
+)
+
+require (
+	github.com/cosi-project/runtime v0.6.4
+	github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0
+	github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475
+	github.com/jhump/grpctunnel v0.3.0
+	github.com/pensando/goipmi v0.0.0-20240603174436-eb122d901c23
+	github.com/pin/tftp/v3 v3.1.0
+	github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587
+	github.com/siderolabs/gen v0.6.1
+	github.com/siderolabs/image-factory v0.5.0
+	github.com/siderolabs/omni/client v0.0.0-20241017162757-284e8b5077cc
+	github.com/siderolabs/talos-metal-agent v0.0.0-20241016074728-46df49991336
+	github.com/spf13/cobra v1.8.1
+	go.uber.org/zap v1.27.0
+	golang.org/x/net v0.30.0
+	golang.org/x/sync v0.8.0
+	google.golang.org/grpc v1.67.1
+	google.golang.org/protobuf v1.35.1
+	gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+	github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton // indirect
+	github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
+	github.com/ProtonMail/gopenpgp/v2 v2.7.5 // indirect
+	github.com/adrg/xdg v0.5.1 // indirect
+	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+	github.com/blang/semver/v4 v4.0.0 // indirect
+	github.com/cenkalti/backoff/v4 v4.3.0 // indirect
+	github.com/cloudflare/circl v1.5.0 // indirect
+	github.com/containerd/go-cni v1.1.10 // indirect
+	github.com/containernetworking/cni v1.2.3 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/fullstorydev/grpchan v1.1.1 // indirect
+	github.com/gertd/go-pluralize v0.2.1 // indirect
+	github.com/google/cel-go v0.21.0 // indirect
+	github.com/google/go-cmp v0.6.0 // indirect
+	github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
+	github.com/hashicorp/errwrap v1.1.0 // indirect
+	github.com/hashicorp/go-multierror v1.1.1 // indirect
+	github.com/inconshreveable/mousetrap v1.1.0 // indirect
+	github.com/josharian/native v1.1.0 // indirect
+	github.com/jsimonetti/rtnetlink/v2 v2.0.2 // indirect
+	github.com/klauspost/compress v1.17.11 // indirect
+	github.com/mdlayher/ethtool v0.2.0 // indirect
+	github.com/mdlayher/genetlink v1.3.2 // indirect
+	github.com/mdlayher/netlink v1.7.2 // indirect
+	github.com/mdlayher/socket v0.5.1 // indirect
+	github.com/opencontainers/runtime-spec v1.2.0 // indirect
+	github.com/pierrec/lz4/v4 v4.1.21 // indirect
+	github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/ryanuber/go-glob v1.0.0 // indirect
+	github.com/siderolabs/crypto v0.5.0 // indirect
+	github.com/siderolabs/go-api-signature v0.3.6 // indirect
+	github.com/siderolabs/go-blockdevice v0.4.8 // indirect
+	github.com/siderolabs/go-blockdevice/v2 v2.0.3 // indirect
+	github.com/siderolabs/go-pointer v1.0.0 // indirect
+	github.com/siderolabs/net v0.4.0 // indirect
+	github.com/siderolabs/proto-codec v0.1.1 // indirect
+	github.com/siderolabs/protoenc v0.2.1 // indirect
+	github.com/siderolabs/talos/pkg/machinery v1.8.1 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/stoewer/go-strcase v1.3.0 // indirect
+	github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/crypto v0.28.0 // indirect
+	golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c // indirect
+	golang.org/x/sys v0.26.0 // indirect
+	golang.org/x/text v0.19.0 // indirect
+	golang.org/x/time v0.7.0 // indirect
+	google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..3476220
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,341 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/ProtonMail/go-crypto v0.0.0-20230717121422-5aa5874ade95/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0=
+github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton h1:ZGewsAoeSirbUS5cO8L0FMQA+iSop9xR1nmFYifDBPo=
+github.com/ProtonMail/go-crypto v1.1.0-beta.0-proton/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
+github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
+github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA=
+github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g=
+github.com/adrg/xdg v0.5.1 h1:Im8iDbEFARltY09yOJlSGu4Asjk2vF85+3Dyru8uJ0U=
+github.com/adrg/xdg v0.5.1/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
+github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
+github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
+github.com/brianvoe/gofakeit/v6 v6.24.0 h1:74yq7RRz/noddscZHRS2T84oHZisW9muwbb8sRnU52A=
+github.com/brianvoe/gofakeit/v6 v6.24.0/go.mod h1:Ow6qC71xtwm79anlwKRlWZW6zVq9D2XHE4QSSMP/rU8=
+github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4=
+github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
+github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
+github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/containerd/go-cni v1.1.10 h1:c2U73nld7spSWfiJwSh/8W9DK+/qQwYM2rngIhCyhyg=
+github.com/containerd/go-cni v1.1.10/go.mod h1:/Y/sL8yqYQn1ZG1om1OncJB1W4zN3YmjfP/ShCzG/OY=
+github.com/containernetworking/cni v1.2.3 h1:hhOcjNVUQTnzdRJ6alC5XF+wd9mfGIUaj8FuJbEslXM=
+github.com/containernetworking/cni v1.2.3/go.mod h1:DuLgF+aPd3DzcTQTtp/Nvl1Kim23oFKdm2okJzBQA5M=
+github.com/cosi-project/runtime v0.6.4 h1:roifc5e+Q1+72EI36BYSRT9aXyskU+coiKHeoBBWkMg=
+github.com/cosi-project/runtime v0.6.4/go.mod h1:EMLs8a55tJ6zA4UyDbRsTvXBd6UIlNwZfCVGvCyiXK8=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls=
+github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
+github.com/fullstorydev/grpchan v1.1.1 h1:heQqIJlAv5Cnks9a70GRL2EJke6QQoUB25VGR6TZQas=
+github.com/fullstorydev/grpchan v1.1.1/go.mod h1:f4HpiV8V6htfY/K44GWV1ESQzHBTq7DinhzqQ95lpgc=
+github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
+github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
+github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
+github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI=
+github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0 h1:pRhl55Yx1eC7BZ1N+BBWwnKaMyD8uC+34TLdndZMAKk=
+github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.1.0/go.mod h1:XKMd7iuf/RGPSMJ/U4HP0zS2Z9Fh8Ps9a+6X26m/tmI=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
+github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
+github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 h1:hxST5pwMBEOWmxpkX20w9oZG+hXdhKmAIPQ3NGGAxas=
+github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475/go.mod h1:KclMyHxX06VrVr0DJmeFSUb1ankt7xTfoOA35pCkoic=
+github.com/jhump/gopoet v0.0.0-20190322174617-17282ff210b3/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/gopoet v0.1.0/go.mod h1:me9yfT6IJSlOL3FCfrg+L6yzUEZ+5jW6WHt4Sk+UPUI=
+github.com/jhump/goprotoc v0.5.0/go.mod h1:VrbvcYrQOrTi3i0Vf+m+oqQWk9l72mjkJCYo7UvLHRQ=
+github.com/jhump/grpctunnel v0.3.0 h1:itddWDKl7J4CeW4nzY3S/a1s7mPZUb8UtUzEhc/R8mg=
+github.com/jhump/grpctunnel v0.3.0/go.mod h1:dn5zls1F+1ftPMkbh4kVTVgGuY5t/v3ZgdjtnSMC3f4=
+github.com/jhump/protoreflect v1.11.0 h1:bvACHUD1Ua/3VxY4aAMpItKMhhwbimlKFJKsLsVgDjU=
+github.com/jhump/protoreflect v1.11.0/go.mod h1:U7aMIjN0NWq9swDP7xDdoMfRHb35uiuTd3Z9nFXJf5E=
+github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
+github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
+github.com/jsimonetti/rtnetlink/v2 v2.0.2 h1:ZKlbCujrIpp4/u3V2Ka0oxlf4BCkt6ojkvpy3nZoCBY=
+github.com/jsimonetti/rtnetlink/v2 v2.0.2/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE=
+github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
+github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/mdlayher/ethtool v0.2.0 h1:akcA4WZVWozzirPASeMq8qgLkxpF3ykftVXwnrMKrhY=
+github.com/mdlayher/ethtool v0.2.0/go.mod h1:W0pIBrNPK1TslIN4Z9wt1EVbay66Kbvek2z2f29VBfw=
+github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
+github.com/mdlayher/genetlink v1.3.2/go.mod h1:tcC3pkCrPUGIKKsCsp0B3AdaaKuHtaxoJRz3cc+528o=
+github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g=
+github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw=
+github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
+github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
+github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
+github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
+github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
+github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk=
+github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
+github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
+github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587 h1:xzZOeCMQLA/W198ZkdVdt4EKFKJtS26B773zNU377ZY=
+github.com/planetscale/vtprotobuf v0.6.1-0.20240917153116-6f2963f01587/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
+github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
+github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4=
+github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY=
+github.com/siderolabs/crypto v0.5.0 h1:+Sox0aYLCcD0PAH2cbEcx557zUrONLtuj1Ws+2MFXGc=
+github.com/siderolabs/crypto v0.5.0/go.mod h1:hsR3tJ3aaeuhCChsLF4dBd9vlJVPvmhg4vvx2ez4aD4=
+github.com/siderolabs/gen v0.6.1 h1:Mex6Q41Tlw3e+4cGvlju2x4UwULD5WMo/D82n7IxV0Y=
+github.com/siderolabs/gen v0.6.1/go.mod h1:an3a2Y53O7kUjnnK8Bfu3gewtvnIOu5RTU6HalFtXQQ=
+github.com/siderolabs/go-api-signature v0.3.6 h1:wDIsXbpl7Oa/FXvxB6uz4VL9INA9fmr3EbmjEZYFJrU=
+github.com/siderolabs/go-api-signature v0.3.6/go.mod h1:hoH13AfunHflxbXfh+NoploqV13ZTDfQ1mQJWNVSW9U=
+github.com/siderolabs/go-blockdevice v0.4.8 h1:KfdWvIx0Jft5YVuCsFIJFwjWEF1oqtzkgX9PeU9cX4c=
+github.com/siderolabs/go-blockdevice v0.4.8/go.mod h1:4PeOuk71pReJj1JQEXDE7kIIQJPVe8a+HZQa+qjxSEA=
+github.com/siderolabs/go-blockdevice/v2 v2.0.3 h1:IEgDqd3H3gPphahrdvfAzU8RmD4r5eQdWC+vgFQQoEg=
+github.com/siderolabs/go-blockdevice/v2 v2.0.3/go.mod h1:74htzCV913UzaLZ4H+NBXkwWlYnBJIq5m/379ZEcu8w=
+github.com/siderolabs/go-pointer v1.0.0 h1:6TshPKep2doDQJAAtHUuHWXbca8ZfyRySjSBT/4GsMU=
+github.com/siderolabs/go-pointer v1.0.0/go.mod h1:HTRFUNYa3R+k0FFKNv11zgkaCLzEkWVzoYZ433P3kHc=
+github.com/siderolabs/go-retry v0.3.3 h1:zKV+S1vumtO72E6sYsLlmIdV/G/GcYSBLiEx/c9oCEg=
+github.com/siderolabs/go-retry v0.3.3/go.mod h1:Ff/VGc7v7un4uQg3DybgrmOWHEmJ8BzZds/XNn/BqMI=
+github.com/siderolabs/goipmi v0.0.0-20211214143420-35f956689e67 h1:R22ZIQgXriopn8zTKnya8JWbEEx2AdgTyKL92hxdJoU=
+github.com/siderolabs/goipmi v0.0.0-20211214143420-35f956689e67/go.mod h1:Vr1Oadtcem03hG2RUT/dpSQS5md9d6rJ9nA0lUBC91Q=
+github.com/siderolabs/image-factory v0.5.0 h1:v1FXZLCcV6xu+6QpgvhDEICxVF7o2VxMjfU0MutkFbo=
+github.com/siderolabs/image-factory v0.5.0/go.mod h1:npJwHOBsI+h+gKdezCyrs7ZHDmkgRnrAK2Cjk1nzv8A=
+github.com/siderolabs/net v0.4.0 h1:1bOgVay/ijPkJz4qct98nHsiB/ysLQU0KLoBC4qLm7I=
+github.com/siderolabs/net v0.4.0/go.mod h1:/ibG+Hm9HU27agp5r9Q3eZicEfjquzNzQNux5uEk0kM=
+github.com/siderolabs/proto-codec v0.1.1 h1:4jiUwW/vaXTZ+YNgZDs37B4aj/1mzV/erIkzUUCRY9g=
+github.com/siderolabs/proto-codec v0.1.1/go.mod h1:rIvmhKJG8+JwSCGPX+cQljpOMDmuHhLKPkt6KaFwEaU=
+github.com/siderolabs/protoenc v0.2.1 h1:BqxEmeWQeMpNP3R6WrPqDatX8sM/r4t97OP8mFmg6GA=
+github.com/siderolabs/protoenc v0.2.1/go.mod h1:StTHxjet1g11GpNAWiATgc8K0HMKiFSEVVFOa/H0otc=
+github.com/siderolabs/talos/pkg/machinery v1.8.1 h1:oeJQmkLNjEG5jxrzPiC2XMQS5dcg1qZ17p5LKcaCbRM=
+github.com/siderolabs/talos/pkg/machinery v1.8.1/go.mod h1:mWTmuUk8G6CdkhUfDmsrIkgPo0G6J5hC/zGazgnyzBg=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og=
+github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
+github.com/utkuozdemir/pin-tftp/v3 v3.0.0-20241021135417-0dd7dba351ad h1:l/GaA5Ut8YynZNW0jBhcE6W4aodMPOyURqpza76lhCw=
+github.com/utkuozdemir/pin-tftp/v3 v3.0.0-20241021135417-0dd7dba351ad/go.mod h1:Kvfewx0+8GO2KpUCPRZAzVjzVN+TnRgn7tuaPlkf3fw=
+github.com/utkuozdemir/sidero-omni/client v0.0.0-20241025225840-6bccefcfa215 h1:8aN9P+6kvmu9TOh6QXhsfO16kSHnrFHgq8v3ts49SWA=
+github.com/utkuozdemir/sidero-omni/client v0.0.0-20241025225840-6bccefcfa215/go.mod h1:cGPkF/BZkjXLMqiea1TRhB+/NtGOPBN7MMzZjUh1C+s=
+github.com/utkuozdemir/sidero-talos-metal-agent v0.0.0-20241028223127-f17157c1b44a h1:pTedjzQFX1pUjXjNbqAA9wO6bQpLwi0m67PdVqB+Xuk=
+github.com/utkuozdemir/sidero-talos-metal-agent v0.0.0-20241028223127-f17157c1b44a/go.mod h1:Qk/3dPwbnt5LTSeY0telKKmNz7AuoMshqh6EUbA+iBA=
+github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
+github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
+golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
+golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
+golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c h1:7dEasQXItcW1xKJ2+gg5VOiBnqWrJc+rq0DPKyvvdbY=
+golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c/go.mod h1:NQtJDoLvd6faHhE7m4T/1IY708gDefGGjR/iUW8yQQ8=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
+golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
+golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
+golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
+golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
+golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
+golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
+golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38 h1:2oV8dfuIkM1Ti7DwXc0BJfnwr9csz4TDXI9EmiI+Rbw=
+google.golang.org/genproto/googleapis/api v0.0.0-20241021214115-324edc3d5d38/go.mod h1:vuAjtvlwkDKF6L1GQ0SokiRLCGFfeBUXWr/aFFkHACc=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
+google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
+google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/hack/release.sh b/hack/release.sh
new file mode 100755
index 0000000..13d9e63
--- /dev/null
+++ b/hack/release.sh
@@ -0,0 +1,149 @@
+#!/usr/bin/env bash
+
+# THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+#
+# Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+set -e
+
+RELEASE_TOOL_IMAGE="ghcr.io/siderolabs/release-tool:latest"
+
+function release-tool {
+  docker pull "${RELEASE_TOOL_IMAGE}" >/dev/null
+  docker run --rm -w /src -v "${PWD}":/src:ro "${RELEASE_TOOL_IMAGE}" -l -d -n -t "${1}" ./hack/release.toml
+}
+
+function changelog {
+  if [ "$#" -eq 1 ]; then
+    (release-tool ${1}; echo; cat CHANGELOG.md) > CHANGELOG.md- && mv CHANGELOG.md- CHANGELOG.md
+  else
+    echo 1>&2 "Usage: $0 changelog [tag]"
+    exit 1
+  fi
+}
+
+function release-notes {
+  release-tool "${2}" > "${1}"
+}
+
+function cherry-pick {
+  if [ $# -ne 2 ]; then
+    echo 1>&2 "Usage: $0 cherry-pick <commit> <branch>"
+    exit 1
+  fi
+
+  git checkout $2
+  git fetch
+  git rebase upstream/$2
+  git cherry-pick -x $1
+}
+
+function commit {
+  if [ $# -ne 1 ]; then
+    echo 1>&2 "Usage: $0 commit <tag>"
+    exit 1
+  fi
+
+  if is_on_main_branch; then
+    update_license_files
+  fi
+
+  git commit -s -m "release($1): prepare release" -m "This is the official $1 release."
+}
+
+function is_on_main_branch {
+  main_remotes=("upstream" "origin")
+  branch_names=("main" "master")
+  current_branch=$(git rev-parse --abbrev-ref HEAD)
+
+  echo "Check current branch: $current_branch"
+
+  for remote in "${main_remotes[@]}"; do
+    echo "Fetch remote $remote..."
+
+    if ! git fetch --quiet "$remote" &>/dev/null; then
+      echo "Failed to fetch $remote, skip..."
+
+      continue
+    fi
+
+    for branch_name in "${branch_names[@]}"; do
+      if ! git rev-parse --verify "$branch_name" &>/dev/null; then
+        echo "Branch $branch_name does not exist, skip..."
+
+        continue
+      fi
+
+      echo "Branch $remote/$branch_name exists, comparing..."
+
+      merge_base=$(git merge-base "$current_branch" "$remote/$branch_name")
+      latest_main=$(git rev-parse "$remote/$branch_name")
+
+      if [ "$merge_base" = "$latest_main" ]; then
+        echo "Current branch is up-to-date with $remote/$branch_name"
+
+        return 0
+      else
+        echo "Current branch is not on $remote/$branch_name"
+
+        return 1
+      fi
+    done
+  done
+
+  echo "No main or master branch found on any remote"
+
+  return 1
+}
+
+function update_license_files {
+  script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+  parent_dir="$(dirname "$script_dir")"
+  current_year=$(date +"%Y")
+  change_date=$(date -v+4y +"%Y-%m-%d" 2>/dev/null || date -d "+4 years" +"%Y-%m-%d" 2>/dev/null || date --date="+4 years" +"%Y-%m-%d")
+
+  # Find LICENSE and .kres.yaml files recursively in the parent directory (project root)
+  find "$parent_dir" \( -name "LICENSE" -o -name ".kres.yaml" \) -type f | while read -r file; do
+    temp_file="${file}.tmp"
+
+    if [[ $file == *"LICENSE" ]]; then
+      if grep -q "^Business Source License" "$file"; then
+        sed -e "s/The Licensed Work is (c) [0-9]\{4\}/The Licensed Work is (c) $current_year/" \
+          -e "s/Change Date:          [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/Change Date:          $change_date/" \
+          "$file" >"$temp_file"
+      else
+        continue # Not a Business Source License file
+      fi
+    elif [[ $file == *".kres.yaml" ]]; then
+      sed -E 's/^([[:space:]]*)ChangeDate:.*$/\1ChangeDate: "'"$change_date"'"/' "$file" >"$temp_file"
+    fi
+
+    # Check if the file has changed
+    if ! cmp -s "$file" "$temp_file"; then
+      mv "$temp_file" "$file"
+      echo "Updated: $file"
+      git add "$file"
+    else
+      echo "No changes: $file"
+      rm "$temp_file"
+    fi
+  done
+}
+
+if declare -f "$1" > /dev/null
+then
+  cmd="$1"
+  shift
+  $cmd "$@"
+else
+  cat <<EOF
+Usage:
+  commit:        Create the official release commit message (updates BUSL license dates if there is any).
+  cherry-pick:   Cherry-pick a commit into a release branch.
+  changelog:     Update the specified CHANGELOG.
+  release-notes: Create release notes for GitHub release.
+EOF
+
+  exit 1
+fi
+
diff --git a/hack/release.toml b/hack/release.toml
new file mode 100644
index 0000000..b6dfd29
--- /dev/null
+++ b/hack/release.toml
@@ -0,0 +1,11 @@
+# commit to be tagged for the new release
+commit = "HEAD"
+
+project_name = "omni-infra-provider-bare-metal"
+github_repo = "siderolabs/omni-infra-provider-bare-metal"
+match_deps = "^github.com/(siderolabs/[a-zA-Z0-9-]+)$"
+
+# previous = -
+# pre_release = true
+
+# [notes]
diff --git a/internal/agent/controller.go b/internal/agent/controller.go
new file mode 100644
index 0000000..17ee1f0
--- /dev/null
+++ b/internal/agent/controller.go
@@ -0,0 +1,84 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package agent implements the metal agent controller.
+package agent
+
+import (
+	"context"
+
+	"github.com/jhump/grpctunnel"
+	"github.com/jhump/grpctunnel/tunnelpb"
+	agentpb "github.com/siderolabs/talos-metal-agent/api/agent"
+	"go.uber.org/zap"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/metadata"
+)
+
+const (
+	unknownMachineAffinityKey = "unknown"
+	machineIDMetadataKey      = "machine-id"
+)
+
+// Controller controls servers by establishing a reverse GRPC tunnel with them and by sending them commands.
+type Controller struct {
+	logger        *zap.Logger
+	grpcServer    grpc.ServiceRegistrar
+	tunnelHandler *grpctunnel.TunnelServiceHandler
+}
+
+// NewController creates a new agent Controller.
+func NewController(grpcServer grpc.ServiceRegistrar, logger *zap.Logger) *Controller {
+	tunnelHandler := grpctunnel.NewTunnelServiceHandler(
+		grpctunnel.TunnelServiceHandlerOptions{
+			OnReverseTunnelOpen: func(channel grpctunnel.TunnelChannel) {
+				if logger.Core().Enabled(zap.DebugLevel) {
+					logger.Debug("reverse tunnel opened", zap.String("machine_id", machineIDAffinityKey(channel, logger)))
+				}
+			},
+			OnReverseTunnelClose: func(channel grpctunnel.TunnelChannel) {
+				if logger.Core().Enabled(zap.DebugLevel) {
+					logger.Debug("reverse tunnel closed", zap.String("machine_id", machineIDAffinityKey(channel, logger)))
+				}
+			},
+			AffinityKey: func(channel grpctunnel.TunnelChannel) any {
+				return machineIDAffinityKey(channel, logger)
+			},
+		},
+	)
+
+	tunnelpb.RegisterTunnelServiceServer(grpcServer, tunnelHandler.Service())
+
+	return &Controller{
+		logger:        logger,
+		grpcServer:    grpcServer,
+		tunnelHandler: tunnelHandler,
+	}
+}
+
+// GetPowerManagementInfo retrieves the IPMI information from the server with the given ID.
+func (c *Controller) GetPowerManagementInfo(ctx context.Context, id string) (*agentpb.GetPowerManagementInfoResponse, error) {
+	channel := c.tunnelHandler.KeyAsChannel(id)
+	cli := agentpb.NewAgentServiceClient(channel)
+
+	return cli.GetPowerManagementInfo(ctx, &agentpb.GetPowerManagementInfoRequest{})
+}
+
+func machineIDAffinityKey(channel grpctunnel.TunnelChannel, logger *zap.Logger) string {
+	md, ok := metadata.FromIncomingContext(channel.Context())
+	if !ok {
+		return unknownMachineAffinityKey
+	}
+
+	machineID := md.Get(machineIDMetadataKey)
+	if len(machineID) == 0 {
+		return unknownMachineAffinityKey
+	}
+
+	if len(machineID) > 1 {
+		logger.Warn("multiple machine IDs in metadata", zap.Strings("machine_ids", machineID))
+	}
+
+	return machineID[0]
+}
diff --git a/internal/config/config.go b/internal/config/config.go
new file mode 100644
index 0000000..b41eb5d
--- /dev/null
+++ b/internal/config/config.go
@@ -0,0 +1,86 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package config serves machine configuration to the machines that request it via talos.config kernel argument.
+package config
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"text/template"
+
+	"go.uber.org/zap"
+)
+
+const machineConfigTemplate = `apiVersion: v1alpha1
+kind: SideroLinkConfig
+apiUrl: {{ .APIURL }}
+---
+apiVersion: v1alpha1
+kind: EventSinkConfig
+endpoint: "[fdae:41e4:649b:9303::1]:8090"
+---
+apiVersion: v1alpha1
+kind: KmsgLogConfig
+name: omni-kmsg
+url: "tcp://[fdae:41e4:649b:9303::1]:8092"
+`
+
+// OmniClient is the interface to interact with Omni.
+type OmniClient interface {
+	GetSiderolinkAPIURL(ctx context.Context) (string, error)
+}
+
+// Handler handles machine configuration requests.
+type Handler struct {
+	logger        *zap.Logger
+	machineConfig string
+}
+
+// NewHandler creates a new Handler.
+func NewHandler(ctx context.Context, omniClient OmniClient, logger *zap.Logger) (*Handler, error) {
+	siderolinkAPIURL, err := omniClient.GetSiderolinkAPIURL(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to get siderolink API URL: %w", err)
+	}
+
+	tmpl, err := template.New("machine-config").Parse(machineConfigTemplate)
+	if err != nil {
+		return nil, err
+	}
+
+	var sb strings.Builder
+
+	if err = tmpl.Execute(&sb, struct {
+		APIURL string
+	}{
+		APIURL: siderolinkAPIURL,
+	}); err != nil {
+		return nil, fmt.Errorf("failed to execute template: %w", err)
+	}
+
+	return &Handler{
+		machineConfig: sb.String(),
+		logger:        logger,
+	}, nil
+}
+
+// ServeHTTP serves the machine configuration.
+//
+// URL pattern: http://ip-of-this-provider:50042/config?&u=${uuid}
+//
+// Implements http.Handler interface.
+func (s *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	uuid := req.URL.Query().Get("u")
+
+	s.logger.Info("handle config request", zap.String("uuid", uuid))
+
+	w.WriteHeader(http.StatusOK)
+
+	if _, err := w.Write([]byte(s.machineConfig)); err != nil {
+		s.logger.Error("failed to write response", zap.Error(err))
+	}
+}
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
new file mode 100644
index 0000000..6643701
--- /dev/null
+++ b/internal/constants/constants.go
@@ -0,0 +1,14 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package constants provides constants for the provider package.
+package constants
+
+const (
+	// IPXEPath is the path to the iPXE binaries.
+	IPXEPath = "/var/lib/ipxe"
+
+	// TFTPPath is the path from which the TFTP server serves files.
+	TFTPPath = "/var/lib/tftp"
+)
diff --git a/internal/debug/debug.go b/internal/debug/debug.go
new file mode 100644
index 0000000..70fd22d
--- /dev/null
+++ b/internal/debug/debug.go
@@ -0,0 +1,6 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package debug provides a way to check if the build is a debug build.
+package debug
diff --git a/internal/debug/disabled.go b/internal/debug/disabled.go
new file mode 100644
index 0000000..f8ef20f
--- /dev/null
+++ b/internal/debug/disabled.go
@@ -0,0 +1,10 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//go:build sidero.debug
+
+package debug
+
+// Enabled is set to true when the build is a debug build (WITH_DEBUG=true).
+const Enabled = true
diff --git a/internal/debug/enabled.go b/internal/debug/enabled.go
new file mode 100644
index 0000000..aa26e16
--- /dev/null
+++ b/internal/debug/enabled.go
@@ -0,0 +1,10 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+//go:build !sidero.debug
+
+package debug
+
+// Enabled is set to true when the build is a debug build (WITH_DEBUG=true).
+const Enabled = false
diff --git a/internal/dhcp/dhcp.go b/internal/dhcp/dhcp.go
new file mode 100644
index 0000000..2f5dcc3
--- /dev/null
+++ b/internal/dhcp/dhcp.go
@@ -0,0 +1,6 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package dhcp implements DHCP proxy and other DHCP related functionality.
+package dhcp
diff --git a/internal/dhcp/proxy.go b/internal/dhcp/proxy.go
new file mode 100644
index 0000000..1e66701
--- /dev/null
+++ b/internal/dhcp/proxy.go
@@ -0,0 +1,240 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package dhcp
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"strconv"
+
+	"github.com/insomniacslk/dhcp/dhcpv4"
+	"github.com/insomniacslk/dhcp/dhcpv4/server4"
+	"github.com/insomniacslk/dhcp/iana"
+	"github.com/siderolabs/gen/xslices"
+	"go.uber.org/zap"
+	"golang.org/x/sync/errgroup"
+)
+
+// Proxy is a DHCP proxy server, adding PXE boot options to the DHCP responses.
+type Proxy struct {
+	logger              *zap.Logger
+	apiAdvertiseAddress string
+	apiPort             int
+}
+
+// NewProxy creates a new DHCP proxy server.
+func NewProxy(apiAdvertiseAddress string, apiPort int, logger *zap.Logger) *Proxy {
+	return &Proxy{
+		apiAdvertiseAddress: apiAdvertiseAddress,
+		apiPort:             apiPort,
+		logger:              logger,
+	}
+}
+
+// Run starts the DHCP proxy server.
+func (p *Proxy) Run(ctx context.Context) error {
+	server, err := server4.NewServer(
+		"",
+		nil,
+		p.handlePacket(),
+	)
+	if err != nil {
+		return fmt.Errorf("failed to create DHCP server: %w", err)
+	}
+
+	eg, ctx := errgroup.WithContext(ctx)
+
+	eg.Go(func() error {
+		if err = server.Serve(); err != nil {
+			if errors.Is(err, net.ErrClosed) {
+				return nil
+			}
+
+			return fmt.Errorf("failed to run DHCP server: %w", err)
+		}
+
+		return nil
+	})
+
+	eg.Go(func() error {
+		<-ctx.Done()
+
+		return server.Close()
+	})
+
+	return eg.Wait()
+}
+
+func (p *Proxy) handlePacket() func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
+	return func(conn net.PacketConn, peer net.Addr, m *dhcpv4.DHCPv4) {
+		logger := p.logger.With(zap.String("source", string(m.ClientHWAddr)))
+
+		if err := isBootDHCP(m); err != nil {
+			logger.Info("ignoring packet", zap.Error(err))
+
+			return
+		}
+
+		fwtype, err := validateDHCP(m)
+		if err != nil {
+			logger.Info("invalid packet", zap.Error(err))
+
+			return
+		}
+
+		resp, err := offerDHCP(m, p.apiAdvertiseAddress, p.apiPort, fwtype)
+		if err != nil {
+			logger.Error("failed to construct ProxyDHCP offer", zap.Error(err))
+
+			return
+		}
+
+		logger.Info("offering boot response", zap.String("server", resp.TFTPServerName()), zap.String("boot_filename", resp.BootFileNameOption()))
+
+		_, err = conn.WriteTo(resp.ToBytes(), peer)
+		if err != nil {
+			logger.Error("failure sending response", zap.Error(err))
+		}
+	}
+}
+
+func isBootDHCP(pkt *dhcpv4.DHCPv4) error {
+	if pkt.MessageType() != dhcpv4.MessageTypeDiscover {
+		return fmt.Errorf("packet is %s, not %s", pkt.MessageType(), dhcpv4.MessageTypeDiscover)
+	}
+
+	if pkt.Options[93] == nil {
+		return errors.New("not a PXE boot request (missing option 93)")
+	}
+
+	return nil
+}
+
+func validateDHCP(m *dhcpv4.DHCPv4) (fwtype Firmware, err error) {
+	arches := m.ClientArch()
+
+	for _, arch := range arches {
+		switch arch { //nolint:exhaustive
+		case iana.INTEL_X86PC:
+			fwtype = FirmwareX86PC
+		case iana.EFI_IA32, iana.EFI_X86_64, iana.EFI_BC:
+			fwtype = FirmwareX86EFI
+		case iana.EFI_ARM64:
+			fwtype = FirmwareARMEFI
+		case iana.EFI_X86_HTTP, iana.EFI_X86_64_HTTP:
+			fwtype = FirmwareX86HTTP
+		case iana.EFI_ARM64_HTTP:
+			fwtype = FirmwareARMHTTP
+		}
+	}
+
+	if fwtype == FirmwareUnsupported {
+		return 0, fmt.Errorf("unsupported client arch: %v", xslices.Map(arches, func(a iana.Arch) string { return a.String() }))
+	}
+
+	// Now, identify special sub-breeds of client firmware based on
+	// the user-class option. Note these only change the "firmware
+	// type", not the architecture we're reporting to Booters. We need
+	// to identify these as part of making the internal chainloading
+	// logic work properly.
+	if userClasses := m.UserClass(); len(userClasses) > 0 {
+		// If the client has had iPXE burned into its ROM (or is a VM
+		// that uses iPXE as the PXE "ROM"), special handling is
+		// needed because in this mode the client is using iPXE native
+		// drivers and chainloading to a UNDI stack won't work.
+		if userClasses[0] == "iPXE" && fwtype == FirmwareX86PC {
+			fwtype = FirmwareX86Ipxe
+		}
+	}
+
+	guid := m.GetOneOption(dhcpv4.OptionClientMachineIdentifier)
+	switch len(guid) {
+	case 0:
+		// A missing GUID is invalid according to the spec, however
+		// there are PXE ROMs in the wild that omit the GUID and still
+		// expect to boot. The only thing we do with the GUID is
+		// mirror it back to the client if it's there, so we might as
+		// well accept these buggy ROMs.
+	case 17:
+		if guid[0] != 0 {
+			return 0, errors.New("malformed client GUID (option 97), leading byte must be zero")
+		}
+	default:
+		return 0, errors.New("malformed client GUID (option 97), wrong size")
+	}
+
+	return fwtype, nil
+}
+
+func offerDHCP(req *dhcpv4.DHCPv4, apiAdvertiseAddress string, apiPort int, fwtype Firmware) (*dhcpv4.DHCPv4, error) {
+	serverIP := net.ParseIP(apiAdvertiseAddress)
+	ipPort := net.JoinHostPort(serverIP.String(), strconv.Itoa(apiPort))
+
+	modifiers := []dhcpv4.Modifier{
+		dhcpv4.WithServerIP(serverIP),
+		dhcpv4.WithOptionCopied(req, dhcpv4.OptionClientMachineIdentifier),
+		dhcpv4.WithOptionCopied(req, dhcpv4.OptionClassIdentifier),
+	}
+
+	resp, err := dhcpv4.NewReplyFromRequest(req,
+		modifiers...,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	if resp.GetOneOption(dhcpv4.OptionClassIdentifier) == nil {
+		resp.UpdateOption(dhcpv4.OptClassIdentifier("PXEClient"))
+	}
+
+	switch fwtype {
+	case FirmwareX86PC:
+		// This is completely standard PXE: just load a file from TFTP.
+		resp.UpdateOption(dhcpv4.OptTFTPServerName(serverIP.String()))
+		resp.UpdateOption(dhcpv4.OptBootFileName("undionly.kpxe"))
+	case FirmwareX86Ipxe:
+		// Almost standard PXE, but the boot filename needs to be a URL.
+		resp.UpdateOption(dhcpv4.OptBootFileName(fmt.Sprintf("tftp://%s/undionly.kpxe", serverIP)))
+	case FirmwareX86EFI:
+		// This is completely standard PXE: just load a file from TFTP.
+		resp.UpdateOption(dhcpv4.OptTFTPServerName(serverIP.String()))
+		resp.UpdateOption(dhcpv4.OptBootFileName("snp.efi"))
+	case FirmwareARMEFI:
+		// This is completely standard PXE: just load a file from TFTP.
+		resp.UpdateOption(dhcpv4.OptTFTPServerName(serverIP.String()))
+		resp.UpdateOption(dhcpv4.OptBootFileName("snp-arm64.efi"))
+	case FirmwareX86HTTP:
+		// This is completely standard HTTP-boot: just load a file from HTTP.
+		resp.UpdateOption(dhcpv4.OptBootFileName(fmt.Sprintf("http://%s/tftp/snp.efi", ipPort)))
+	case FirmwareARMHTTP:
+		// This is completely standard HTTP-boot: just load a file from HTTP.
+		resp.UpdateOption(dhcpv4.OptBootFileName(fmt.Sprintf("http://%s/tftp/snp-arm64.efi", ipPort)))
+	case FirmwareUnsupported:
+		fallthrough
+	default:
+		return nil, fmt.Errorf("unsupported firmware type %d", fwtype)
+	}
+
+	return resp, nil
+}
+
+// Firmware describes a kind of firmware attempting to boot.
+//
+// This should only be used for selecting the right bootloader,
+// kernel selection should key off the more generic architecture.
+type Firmware int
+
+// The bootloaders that we know how to handle.
+const (
+	FirmwareUnsupported Firmware = iota // Unsupported
+	FirmwareX86PC                       // "Classic" x86 BIOS with PXE/UNDI support
+	FirmwareX86EFI                      // EFI x86
+	FirmwareARMEFI                      // EFI ARM64
+	FirmwareX86Ipxe                     // "Classic" x86 BIOS running iPXE (no UNDI support)
+	FirmwareX86HTTP                     // HTTP Boot X86
+	FirmwareARMHTTP                     // ARM64 HTTP Boot
+)
diff --git a/internal/imagefactory/imagefactory.go b/internal/imagefactory/imagefactory.go
new file mode 100644
index 0000000..ec148fe
--- /dev/null
+++ b/internal/imagefactory/imagefactory.go
@@ -0,0 +1,128 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package imagefactory provides an abstraction to the image factory for the bare metal infra provider.
+package imagefactory
+
+import (
+	"context"
+	"fmt"
+	"net"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/siderolabs/image-factory/pkg/client"
+	"github.com/siderolabs/image-factory/pkg/schematic"
+	"github.com/siderolabs/omni/client/pkg/meta"
+	"gopkg.in/yaml.v3"
+)
+
+const metalAgentModeExtension = "siderolabs/metal-agent-mode"
+
+// Client is an image factory client.
+type Client struct {
+	pxeBaseURL        string
+	talosVersion      string
+	machineLabelsMeta string
+	factoryClient     *client.Client
+	talosConfigURL    string
+}
+
+// NewClient creates a new image factory client.
+func NewClient(baseURL, pxeBaseURL, talosVersion, apiAdvertiseAddress string, apiPort int, machineLabels []string) (*Client, error) {
+	labelsMeta, err := parseLabels(machineLabels)
+	if err != nil {
+		return nil, err
+	}
+
+	factoryClient, err := client.New(baseURL)
+	if err != nil {
+		return nil, err
+	}
+
+	talosConfigURL := fmt.Sprintf("https://%s/config?u={uuid}", net.JoinHostPort(apiAdvertiseAddress, strconv.Itoa(apiPort)))
+
+	return &Client{
+		pxeBaseURL:        pxeBaseURL,
+		talosVersion:      talosVersion,
+		machineLabelsMeta: labelsMeta,
+		talosConfigURL:    talosConfigURL,
+
+		factoryClient: factoryClient,
+	}, nil
+}
+
+// SchematicIPXEURL ensures a schematic exists on the image factory and returns the iPXE URL to it.
+//
+// If agentMode is true, the schematic will be created with the metal-agent-mode extension.
+func (c *Client) SchematicIPXEURL(ctx context.Context, agentMode bool) (string, error) {
+	var (
+		metaValues         []schematic.MetaValue
+		officialExtensions []string
+	)
+
+	if c.machineLabelsMeta != "" {
+		metaValues = append(metaValues, schematic.MetaValue{
+			Key:   meta.LabelsMeta,
+			Value: c.machineLabelsMeta,
+		})
+	}
+
+	if agentMode {
+		officialExtensions = append(officialExtensions, metalAgentModeExtension)
+	}
+
+	ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
+	defer cancel()
+
+	sch := schematic.Schematic{
+		Customization: schematic.Customization{
+			ExtraKernelArgs: []string{
+				"talos.config=" + c.talosConfigURL,
+			},
+			Meta: metaValues,
+			SystemExtensions: schematic.SystemExtensions{
+				OfficialExtensions: officialExtensions,
+			},
+		},
+	}
+
+	schematicID, err := c.factoryClient.SchematicCreate(ctx, sch)
+	if err != nil {
+		return "", fmt.Errorf("failed to create schematic: %w", err)
+	}
+
+	ipxeURL := fmt.Sprintf("https://%s/pxe/%s/%s/metal-amd64", c.pxeBaseURL, schematicID, c.talosVersion)
+
+	return ipxeURL, err
+}
+
+func parseLabels(machineLabels []string) (string, error) {
+	labels := map[string]string{}
+
+	for _, l := range machineLabels {
+		parts := strings.Split(l, "=")
+		if len(parts) > 2 {
+			return "", fmt.Errorf("malformed label %s", l)
+		}
+
+		value := ""
+
+		if len(parts) > 1 {
+			value = parts[1]
+		}
+
+		labels[parts[0]] = value
+	}
+
+	data, err := yaml.Marshal(meta.ImageLabels{
+		Labels: labels,
+	})
+	if err != nil {
+		return "", err
+	}
+
+	return string(data), nil
+}
diff --git a/internal/ip/ip.go b/internal/ip/ip.go
new file mode 100644
index 0000000..f3cfe65
--- /dev/null
+++ b/internal/ip/ip.go
@@ -0,0 +1,51 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package ip provides IP address related functionality.
+package ip
+
+import (
+	"fmt"
+	"net"
+)
+
+// RoutableIPs returns a list of routable IP addresses.
+func RoutableIPs() ([]string, error) {
+	addresses, err := net.InterfaceAddrs()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get interfaces: %w", err)
+	}
+
+	routableIPs := make([]string, 0, len(addresses))
+
+	for _, addr := range addresses {
+		ipNet, ok := addr.(*net.IPNet)
+		if !ok {
+			continue
+		}
+
+		if isRoutableIP(ipNet.IP) {
+			routableIPs = append(routableIPs, ipNet.IP.String())
+		}
+	}
+
+	return routableIPs, nil
+}
+
+func isRoutableIP(ip net.IP) bool {
+	isReservedIPv4 := func(ip net.IP) bool {
+		return ip[0] >= 240
+	}
+
+	if ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() ||
+		ip.IsMulticast() || ip.IsUnspecified() {
+		return false
+	}
+
+	if ip.To4() != nil {
+		return !isReservedIPv4(ip)
+	}
+
+	return true
+}
diff --git a/internal/ipxe/ipxe.go b/internal/ipxe/ipxe.go
new file mode 100644
index 0000000..63d5aba
--- /dev/null
+++ b/internal/ipxe/ipxe.go
@@ -0,0 +1,137 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package ipxe provides iPXE functionality.
+package ipxe
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/cosi-project/runtime/pkg/state"
+	"go.uber.org/zap"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/resources"
+)
+
+const ipxeScriptTemplateFormat = `#!ipxe
+chain --replace %s
+`
+
+// OmniClient represents an Omni client.
+type OmniClient interface {
+	GetMachine(ctx context.Context, id string) (*resources.Machine, error)
+}
+
+// ImageFactoryClient represents an image factory client which ensures a schematic exists on image factory, and returns the PXE URL to it.
+type ImageFactoryClient interface {
+	SchematicIPXEURL(ctx context.Context, agentMode bool) (string, error)
+}
+
+// Handler represents an iPXE handler.
+type Handler struct {
+	omniClient         OmniClient
+	imageFactoryClient ImageFactoryClient
+
+	logger *zap.Logger
+
+	agentTalosImage string
+}
+
+// ServeHTTP serves the iPXE request.
+//
+// URL pattern: http://ip-of-this-provider:50042/ipxe?uuid=${uuid}&mac=${net${idx}/mac:hexhyp}&domain=${domain}&hostname=${hostname}&serial=${serial}&arch=${buildarch}
+//
+// Implements http.Handler interface.
+func (handler *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	uuid := req.URL.Query().Get("uuid")
+	mac := req.URL.Query().Get("mac")
+	domain := req.URL.Query().Get("domain")
+	hostname := req.URL.Query().Get("hostname")
+	serial := req.URL.Query().Get("serial")
+	arch := req.URL.Query().Get("arch")
+
+	handler.logger.Info("handle iPXE request", zap.String("uuid", uuid), zap.String("mac", mac),
+		zap.String("domain", domain), zap.String("hostname", hostname), zap.String("serial", serial), zap.String("arch", arch))
+
+	// decide if we need to boot the machine into the agent mode
+	_, err := handler.omniClient.GetMachine(req.Context(), uuid)
+	if err != nil {
+		if !state.IsNotFoundError(err) {
+			handler.logger.Error("failed to get machine", zap.Error(err))
+
+			w.WriteHeader(http.StatusInternalServerError)
+			w.Write([]byte("failed to get machine")) //nolint:errcheck
+
+			return
+		}
+
+		useImageFactory := handler.agentTalosImage == ""
+
+		if useImageFactory {
+			handler.serveFactoryIPXEScript(w, req, true)
+
+			return
+		}
+
+		// todo: boot with local assets in the agent talos image
+
+		return
+	}
+
+	// boot into regular Talos over the image factory
+
+	ipxeURL, schematicErr := handler.imageFactoryClient.SchematicIPXEURL(req.Context(), false)
+	if schematicErr != nil {
+		handler.logger.Error("failed to get schematic IPXE URL", zap.Error(schematicErr))
+
+		w.WriteHeader(http.StatusInternalServerError)
+
+		w.Write([]byte("failed to get schematic IPXE URL")) //nolint:errcheck
+
+		return
+	}
+
+	ipxeScript := fmt.Sprintf(ipxeScriptTemplateFormat, ipxeURL)
+
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte(ipxeScript)) //nolint:errcheck
+}
+
+func (handler *Handler) serveFactoryIPXEScript(w http.ResponseWriter, req *http.Request, agentMode bool) {
+	ipxeURL, schematicErr := handler.imageFactoryClient.SchematicIPXEURL(req.Context(), agentMode)
+	if schematicErr != nil {
+		handler.logger.Error("failed to get schematic IPXE URL", zap.Error(schematicErr))
+
+		w.WriteHeader(http.StatusInternalServerError)
+
+		w.Write([]byte("failed to get schematic IPXE URL")) //nolint:errcheck
+
+		return
+	}
+
+	ipxeScript := fmt.Sprintf(ipxeScriptTemplateFormat, ipxeURL)
+
+	w.WriteHeader(http.StatusOK)
+	w.Write([]byte(ipxeScript)) //nolint:errcheck
+}
+
+// NewHandler creates a new iPXE server.
+func NewHandler(imageFactoryClient ImageFactoryClient, omniClient OmniClient, endpoint string, port int, agentTalosImage string, logger *zap.Logger) (*Handler, error) {
+	logger.Info("patch iPXE binaries")
+
+	if err := patchBinaries(endpoint, port); err != nil {
+		return nil, err
+	}
+
+	logger.Info("successfully patched iPXE binaries")
+
+	return &Handler{
+		omniClient:         omniClient,
+		imageFactoryClient: imageFactoryClient,
+		agentTalosImage:    agentTalosImage,
+		logger:             logger,
+	}, nil
+}
diff --git a/internal/ipxe/patch.go b/internal/ipxe/patch.go
new file mode 100644
index 0000000..1ea7bd4
--- /dev/null
+++ b/internal/ipxe/patch.go
@@ -0,0 +1,205 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+package ipxe
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"text/template"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/constants"
+)
+
+// bootTemplate is embedded into iPXE binary when that binary is sent to the node.
+//
+//nolint:dupword
+var bootTemplate = template.Must(template.New("iPXE embedded").Parse(`#!ipxe
+prompt --key 0x02 --timeout 2000 Press Ctrl-B for the iPXE command line... && shell ||
+
+{{/* print interfaces */}}
+ifstat
+
+{{/* retry 10 times overall */}}
+set attempts:int32 10
+set x:int32 0
+
+:retry_loop
+
+	set idx:int32 0
+
+	:loop
+		{{/* try DHCP on each interface */}}
+		isset ${net${idx}/mac} || goto exhausted
+
+		ifclose
+		iflinkwait --timeout 5000 net${idx} || goto next_iface
+		dhcp net${idx} || goto next_iface
+		goto boot
+
+	:next_iface
+		inc idx && goto loop
+
+	:boot
+		{{/* attempt boot, if fails try next iface */}}
+		route
+
+		chain --replace http://{{ .Endpoint }}:{{ .Port }}/ipxe?uuid=${uuid}&mac=${net${idx}/mac:hexhyp}&domain=${domain}&hostname=${hostname}&serial=${serial}&arch=${buildarch} || goto next_iface
+
+:exhausted
+	echo
+	echo Failed to iPXE boot successfully via all interfaces
+
+	iseq ${x} ${attempts} && goto fail ||
+
+	echo Retrying...
+	echo
+
+	inc x
+	goto retry_loop
+
+:fail
+	echo
+	echo Failed to get a valid response after ${attempts} attempts
+	echo
+
+	echo Rebooting in 5 seconds...
+	sleep 5
+	reboot
+`))
+
+func buildBootScript(endpoint string, port int) ([]byte, error) {
+	var buf bytes.Buffer
+
+	if err := bootTemplate.Execute(&buf, struct {
+		Endpoint string
+		Port     int
+	}{
+		Endpoint: endpoint,
+		Port:     port,
+	}); err != nil {
+		return nil, err
+	}
+
+	return buf.Bytes(), nil
+}
+
+// patchBinaries patches iPXE binaries on the fly with the new embedded script.
+//
+// This relies on special build in `pkgs/ipxe` where a placeholder iPXE script is embedded.
+// EFI iPXE binaries are uncompressed, so these are patched directly.
+// BIOS amd64 undionly.pxe is compressed, so we instead patch uncompressed version and compress it back using zbin.
+// (zbin is built with iPXE).
+func patchBinaries(endpoint string, port int) error {
+	bootScript, err := buildBootScript(endpoint, port)
+	if err != nil {
+		return fmt.Errorf("failed to build boot script: %w", err)
+	}
+
+	for _, name := range []string{"ipxe", "snp"} {
+		if err = patchScript(
+			fmt.Sprintf(constants.IPXEPath+"/amd64/%s.efi", name),
+			fmt.Sprintf(constants.TFTPPath+"/%s.efi", name),
+			bootScript,
+		); err != nil {
+			return fmt.Errorf("failed to patch %q: %w", name, err)
+		}
+
+		if err = patchScript(
+			fmt.Sprintf(constants.IPXEPath+"/arm64/%s.efi", name),
+			fmt.Sprintf(constants.TFTPPath+"/%s-arm64.efi", name),
+			bootScript,
+		); err != nil {
+			return fmt.Errorf("failed to patch %q: %w", name, err)
+		}
+	}
+
+	if err = patchScript(constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.bin", constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.bin.patched", bootScript); err != nil {
+		return fmt.Errorf("failed to patch undionly.kpxe.bin: %w", err)
+	}
+
+	if err = compressKPXE(constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.bin.patched", constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.zinfo", constants.TFTPPath+"/undionly.kpxe"); err != nil {
+		return fmt.Errorf("failed to compress undionly.kpxe: %w", err)
+	}
+
+	if err = compressKPXE(constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.bin.patched", constants.IPXEPath+"/amd64/kpxe/undionly.kpxe.zinfo", constants.TFTPPath+"/undionly.kpxe.0"); err != nil {
+		return fmt.Errorf("failed to compress undionly.kpxe.0: %w", err)
+	}
+
+	return nil
+}
+
+var (
+	placeholderStart = []byte("# *PLACEHOLDER START*")
+	placeholderEnd   = []byte("# *PLACEHOLDER END*")
+)
+
+func patchScript(source, destination string, script []byte) error {
+	contents, err := os.ReadFile(source)
+	if err != nil {
+		return err
+	}
+
+	start := bytes.Index(contents, placeholderStart)
+	if start == -1 {
+		return fmt.Errorf("placeholder start not found in %q", source)
+	}
+
+	end := bytes.Index(contents, placeholderEnd)
+	if end == -1 {
+		return fmt.Errorf("placeholder end not found in %q", source)
+	}
+
+	if end < start {
+		return fmt.Errorf("placeholder end before start")
+	}
+
+	end += len(placeholderEnd)
+
+	length := end - start
+
+	if len(script) > length {
+		return fmt.Errorf("script size %d is larger than placeholder space %d", len(script), length)
+	}
+
+	script = append(script, bytes.Repeat([]byte{'\n'}, length-len(script))...)
+
+	copy(contents[start:end], script)
+
+	if err = os.MkdirAll(filepath.Dir(destination), 0o755); err != nil {
+		return err
+	}
+
+	return os.WriteFile(destination, contents, 0o644)
+}
+
+// compressKPXE is equivalent to: ./util/zbin bin/undionly.kpxe.bin bin/undionly.kpxe.zinfo > bin/undionly.kpxe.zbin.
+func compressKPXE(binFile, infoFile, outFile string) error {
+	out, err := os.Create(outFile)
+	if err != nil {
+		return err
+	}
+
+	defer out.Close() //nolint:errcheck
+
+	cmd := exec.Command("/bin/zbin", binFile, infoFile)
+	cmd.Stdout = out
+
+	err = cmd.Run()
+	if err != nil {
+		var exitErr *exec.ExitError
+
+		if errors.As(err, &exitErr) {
+			return fmt.Errorf("zbin failed with exit code %d, stderr: %v", exitErr.ExitCode(), string(exitErr.Stderr))
+		}
+
+		return fmt.Errorf("failed to run zbin: %w", err)
+	}
+
+	return nil
+}
diff --git a/internal/meta/meta.go b/internal/meta/meta.go
new file mode 100644
index 0000000..c8bf8f0
--- /dev/null
+++ b/internal/meta/meta.go
@@ -0,0 +1,9 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package meta contains meta information about the provider.
+package meta
+
+// ProviderID is the ID of the provider.
+var ProviderID = "bare-metal"
diff --git a/internal/omni/omni.go b/internal/omni/omni.go
new file mode 100644
index 0000000..720ceff
--- /dev/null
+++ b/internal/omni/omni.go
@@ -0,0 +1,188 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package omni provides Omni-related functionality.
+package omni
+
+import (
+	"context"
+	"encoding/base64"
+	"fmt"
+	"os"
+
+	"github.com/cosi-project/runtime/pkg/safe"
+	"github.com/cosi-project/runtime/pkg/state"
+	"github.com/siderolabs/omni/client/pkg/client"
+	"github.com/siderolabs/omni/client/pkg/jointoken"
+	"github.com/siderolabs/omni/client/pkg/omni/resources/infra"
+	"github.com/siderolabs/omni/client/pkg/omni/resources/omni"
+	"github.com/siderolabs/omni/client/pkg/omni/resources/siderolink"
+	"go.uber.org/zap"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/status"
+
+	providerpb "github.com/siderolabs/omni-infra-provider-bare-metal/api/provider"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/specs"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/meta"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/resources"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/service"
+)
+
+// Client is a wrapper around the Omni client.
+type Client struct {
+	omniClient *client.Client
+}
+
+// BuildClient creates a new Omni client.
+func BuildClient(endpoint string, insecureSkipTLSVerify bool) (*Client, error) {
+	serviceAccountKey := os.Getenv("OMNI_SERVICE_ACCOUNT_KEY")
+
+	cliOpts := []client.Option{
+		client.WithInsecureSkipTLSVerify(insecureSkipTLSVerify),
+	}
+
+	if serviceAccountKey != "" {
+		cliOpts = append(cliOpts, client.WithServiceAccount(serviceAccountKey))
+	}
+
+	omniClient, err := client.New(endpoint, cliOpts...)
+	if err != nil {
+		return nil, fmt.Errorf("failed to create Omni client: %w", err)
+	}
+
+	return &Client{omniClient: omniClient}, nil
+}
+
+// Close closes the Omni client.
+func (c *Client) Close() error {
+	return c.omniClient.Close()
+}
+
+// GetSiderolinkAPIURL returns the SideroLink API URL.
+func (c *Client) GetSiderolinkAPIURL(ctx context.Context) (string, error) {
+	st := c.omniClient.Omni().State()
+
+	connectionParams, err := safe.StateGetByID[*siderolink.ConnectionParams](ctx, st, siderolink.ConfigID)
+	if err != nil {
+		return "", fmt.Errorf("failed to get connection params: %w", err)
+	}
+
+	token, err := jointoken.NewWithExtraData(connectionParams.TypedSpec().Value.JoinToken, map[string]string{
+		omni.LabelInfraProviderID: meta.ProviderID, // go to omni, don't do the check of MachineReqStatus
+	})
+	if err != nil {
+		return "", err
+	}
+
+	tokenString, err := token.Encode()
+	if err != nil {
+		return "", fmt.Errorf("failed to encode the siderolink token: %w", err)
+	}
+
+	apiURL, err := siderolink.APIURL(connectionParams, siderolink.WithJoinToken(tokenString))
+	if err != nil {
+		return "", fmt.Errorf("failed to build API URL: %w", err)
+	}
+
+	return apiURL, nil
+}
+
+// EnsureProviderStatus makes sure that the infra.ProviderStatus resource exists and is up to date for this provider.
+func (c *Client) EnsureProviderStatus(ctx context.Context, name, description string, rawIcon []byte) error {
+	populate := func(res *infra.ProviderStatus) {
+		res.Metadata().Labels().Set(omni.LabelIsStaticInfraProvider, "")
+
+		res.TypedSpec().Value.Name = name
+		res.TypedSpec().Value.Description = description
+		res.TypedSpec().Value.Icon = base64.RawStdEncoding.EncodeToString(rawIcon)
+	}
+
+	providerStatus := infra.NewProviderStatus(meta.ProviderID)
+
+	populate(providerStatus)
+
+	st := c.omniClient.Omni().State()
+
+	if err := st.Create(ctx, providerStatus); err != nil {
+		if !state.IsConflictError(err) {
+			return err
+		}
+
+		if _, err = safe.StateUpdateWithConflicts(ctx, st, providerStatus.Metadata(), func(res *infra.ProviderStatus) error {
+			populate(res)
+
+			return nil
+		}); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// RunReverseTunnel starts the reverse GRPC tunnel to Omni.
+func (c *Client) RunReverseTunnel(ctx context.Context, powerManager service.PowerManager, logger *zap.Logger) error {
+	reverseTunnelServer := c.omniClient.Tunnel()
+	providerServiceServer := service.NewProviderServiceServer(powerManager, logger)
+
+	providerpb.RegisterProviderServiceServer(reverseTunnelServer, providerServiceServer)
+
+	// Open the reverse tunnel and serve requests.
+	if _, err := reverseTunnelServer.Serve(ctx); err != nil {
+		if status.Code(err) == codes.Canceled {
+			return nil
+		}
+
+		return fmt.Errorf("failed to serve reverse tunnel: %w", err)
+	}
+
+	return nil
+}
+
+// GetMachine returns the machine with the given ID from the persistent state.
+func (c *Client) GetMachine(ctx context.Context, id string) (*resources.Machine, error) {
+	machine, err := safe.StateGetByID[*resources.Machine](ctx, c.omniClient.Omni().State(), id)
+	if err != nil {
+		return nil, err
+	}
+
+	return machine, nil
+}
+
+// SaveMachine saves the machine with the given ID and spec to the persistent state.
+func (c *Client) SaveMachine(ctx context.Context, id string, spec *specs.MachineSpec) (*resources.Machine, error) {
+	st := c.omniClient.Omni().State()
+	machine := resources.NewMachine(id)
+	machine.TypedSpec().Value = spec
+
+	if err := st.Create(ctx, machine); err != nil {
+		if !state.IsConflictError(err) {
+			return nil, err
+		}
+
+		machine, err = safe.StateUpdateWithConflicts(ctx, st, machine.Metadata(), func(res *resources.Machine) error {
+			res.TypedSpec().Value = spec
+
+			return nil
+		})
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return machine, nil
+}
+
+// RemoveMachine removes the machine from the persistent state with the given ID.
+func (c *Client) RemoveMachine(ctx context.Context, id string) error {
+	st := c.omniClient.Omni().State()
+
+	if err := st.Destroy(ctx, resources.NewMachine(id).Metadata()); err != nil {
+		if !state.IsNotFoundError(err) {
+			return err
+		}
+	}
+
+	return nil
+}
diff --git a/internal/power/api/api.go b/internal/power/api/api.go
new file mode 100644
index 0000000..d89528a
--- /dev/null
+++ b/internal/power/api/api.go
@@ -0,0 +1,60 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package api provides power management functionality using an HTTP API, e.g., the HTTP API run by 'talosctl cluster create'.
+package api
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/specs"
+)
+
+// Client is an API power management client: it communicates with an HTTP API to send power management commands.
+type Client struct {
+	rebootEndpoint string
+}
+
+// Close implements the power.Client interface.
+func (c *Client) Close() error {
+	return nil
+}
+
+// Reboot implements the power.Client interface.
+func (c *Client) Reboot(ctx context.Context) error {
+	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
+	defer cancel()
+
+	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.rebootEndpoint, nil)
+	if err != nil {
+		return fmt.Errorf("failed to create reboot request: %w", err)
+	}
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return fmt.Errorf("failed to make reboot request: %w", err)
+	}
+
+	defer resp.Body.Close() //nolint:errcheck
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("unexpected status code while resetting machine: %d", resp.StatusCode)
+	}
+
+	return nil
+}
+
+// NewClient creates a new API power management client.
+func NewClient(info *specs.MachineSpec_APIInfo) (*Client, error) {
+	rebootEndpoint, err := url.JoinPath(info.Address, "/reboot")
+	if err != nil {
+		return nil, fmt.Errorf("failed to create reboot endpoint: %w", err)
+	}
+
+	return &Client{rebootEndpoint: rebootEndpoint}, nil
+}
diff --git a/internal/power/ipmi/ipmi.go b/internal/power/ipmi/ipmi.go
new file mode 100644
index 0000000..6fd3d47
--- /dev/null
+++ b/internal/power/ipmi/ipmi.go
@@ -0,0 +1,49 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package ipmi provides power management functionality using IPMI.
+package ipmi
+
+import (
+	"context"
+
+	goipmi "github.com/pensando/goipmi"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/specs"
+)
+
+const ipmiUsername = "talos-agent"
+
+// Client is a wrapper around the goipmi client.
+type Client struct {
+	ipmiClient *goipmi.Client
+}
+
+// Close implements the power.Client interface.
+func (c *Client) Close() error {
+	return c.ipmiClient.Close()
+}
+
+// Reboot implements the power.Client interface.
+func (c *Client) Reboot(context.Context) error {
+	return c.ipmiClient.Control(goipmi.ControlPowerCycle)
+}
+
+// NewClient creates a new IPMI client.
+func NewClient(info *specs.MachineSpec_IPMIInfo) (*Client, error) {
+	conn := &goipmi.Connection{
+		Hostname:  info.Ip,
+		Port:      int(info.Port),
+		Username:  ipmiUsername,
+		Password:  info.Password,
+		Interface: "lanplus",
+	}
+
+	client, err := goipmi.NewClient(conn)
+	if err != nil {
+		return nil, err
+	}
+
+	return &Client{ipmiClient: client}, nil
+}
diff --git a/internal/power/power.go b/internal/power/power.go
new file mode 100644
index 0000000..4ad8e2c
--- /dev/null
+++ b/internal/power/power.go
@@ -0,0 +1,70 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package power provides power management functionality for machines.
+package power
+
+import (
+	"context"
+	"fmt"
+	"io"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/power/api"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/power/ipmi"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/resources"
+)
+
+// Client is the interface to interact with a single machine to send power commands to it.
+type Client interface {
+	io.Closer
+	Reboot(ctx context.Context) error
+}
+
+// OmniClient is the interface to manage persisted resources.Machine resources.
+type OmniClient interface {
+	GetMachine(ctx context.Context, id string) (*resources.Machine, error)
+}
+
+// Manager manages power operations for machines.
+type Manager struct {
+	omniClient OmniClient
+}
+
+// NewManager creates a new Manager.
+func NewManager(omniClient OmniClient) *Manager {
+	return &Manager{
+		omniClient: omniClient,
+	}
+}
+
+// Reboot reboots the machine with the given ID.
+func (m *Manager) Reboot(ctx context.Context, id string) error {
+	machine, err := m.omniClient.GetMachine(ctx, id)
+	if err != nil {
+		return err
+	}
+
+	client, err := m.getClient(machine)
+	if err != nil {
+		return err
+	}
+
+	defer client.Close() //nolint:errcheck
+
+	return client.Reboot(ctx)
+}
+
+func (m *Manager) getClient(machine *resources.Machine) (Client, error) {
+	apiInfo := machine.TypedSpec().Value.Api
+	if apiInfo != nil {
+		return api.NewClient(apiInfo)
+	}
+
+	ipmiInfo := machine.TypedSpec().Value.Ipmi
+	if ipmiInfo != nil {
+		return ipmi.NewClient(ipmiInfo)
+	}
+
+	return nil, fmt.Errorf("no power client found for machine %s", machine.Metadata().ID())
+}
diff --git a/internal/provider/data/icon.svg b/internal/provider/data/icon.svg
new file mode 100644
index 0000000..74fc065
--- /dev/null
+++ b/internal/provider/data/icon.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 203.74 226.05"><defs><style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:url(#linear-gradient-3);}.cls-4{fill:url(#linear-gradient-4);}.cls-5{fill:url(#linear-gradient-5);}</style><linearGradient id="linear-gradient" x1="101.85" y1="-12.91" x2="101.85" y2="224.04" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#ffd200"/><stop offset="0.08" stop-color="#ffb500"/><stop offset="0.2" stop-color="#ff8c00"/><stop offset="0.3" stop-color="#ff7300"/><stop offset="0.36" stop-color="#ff6a00"/><stop offset="0.48" stop-color="#fc4f0e"/><stop offset="0.65" stop-color="#f92f1e"/><stop offset="0.79" stop-color="#f81b27"/><stop offset="0.89" stop-color="#f7142b"/><stop offset="1" stop-color="#df162e"/></linearGradient><linearGradient id="linear-gradient-2" x1="24.84" y1="-12.91" x2="24.84" y2="224.04" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="178.9" y1="-12.91" x2="178.9" y2="224.04" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-4" x1="145.06" y1="-12.91" x2="145.06" y2="224.04" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-5" x1="58.64" y1="-12.91" x2="58.64" y2="224.04" xlink:href="#linear-gradient"/></defs><g id="Layer_2" data-name="Layer 2"><g id="Layer_1-2" data-name="Layer 1"><path class="cls-1" d="M101.89,226.05c2.85,0,5.67-.15,8.46-.35V.35c-2.8-.21-5.62-.35-8.48-.35s-5.7.14-8.52.35V225.69c2.81.21,5.64.35,8.5.36Z"/><path class="cls-2" d="M11.56,50.9,9.12,48.47A112.82,112.82,0,0,0,.2,63.61c29.42,29.89,32.52,44.31,32.48,49.14C32.57,125,17.58,144.21,0,162a113.69,113.69,0,0,0,8.84,15.15c1-1,1.95-1.92,2.92-2.9,25.37-25.54,37.77-45.61,37.92-61.38S37.36,77,11.56,50.9Z"/><path class="cls-3" d="M192,174.29l2.92,2.9A113.69,113.69,0,0,0,203.74,162c-17.57-17.83-32.56-37.09-32.68-49.29-.11-11.9,14.79-31.15,32.46-49.18a112.88,112.88,0,0,0-8.9-15.1l-2.44,2.43c-25.8,26.05-38.27,46.34-38.12,62S166.61,148.75,192,174.29Z"/><path class="cls-4" d="M140.68,112.83c0-22,9.81-58.58,24.92-93.15A113,113,0,0,0,150.45,11c-16.54,37.27-26.78,76.91-26.78,101.87,0,24.15,11.09,64.23,27.93,101.7a113,113,0,0,0,14.84-8.77C150.85,170.73,140.68,134.07,140.68,112.83Z"/><path class="cls-5" d="M80,112.83C80,87.74,69.35,47.88,53,11.07a112.76,112.76,0,0,0-14.93,8.64C53.21,54.26,63,90.85,63,112.83c0,21.23-10.17,57.88-25.76,92.91a113.66,113.66,0,0,0,14.84,8.77C68.94,177.05,80,137,80,112.83Z"/></g></g></svg>
\ No newline at end of file
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
new file mode 100644
index 0000000..191473a
--- /dev/null
+++ b/internal/provider/provider.go
@@ -0,0 +1,140 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package provider implements the bare metal infra provider.
+package provider
+
+import (
+	"context"
+	_ "embed"
+	"fmt"
+
+	"go.uber.org/zap"
+	"golang.org/x/sync/errgroup"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/agent"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/config"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/dhcp"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/imagefactory"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/ipxe"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/omni"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/power"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/server"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/tftp"
+)
+
+//go:embed data/icon.svg
+var icon []byte
+
+// Options contains the provider options.
+type Options struct {
+	Name                     string
+	Description              string
+	OmniAPIEndpoint          string
+	AgentTalosImage          string
+	ImageFactoryBaseURL      string
+	ImageFactoryPXEBaseURL   string
+	ImageFactoryTalosVersion string
+	APIListenAddress         string
+	APIAdvertiseAddress      string
+	APIPowerMgmtStateDir     string
+	MachineLabels            []string
+	APIPort                  int
+	InsecureSkipTLSVerify    bool
+}
+
+// Provider implements the bare metal infra provider.
+type Provider struct {
+	logger *zap.Logger
+
+	options Options
+}
+
+// New creates a new Provider.
+func New(options Options, logger *zap.Logger) *Provider {
+	return &Provider{
+		options: options,
+		logger:  logger,
+	}
+}
+
+// Run runs the provider.
+func (p *Provider) Run(ctx context.Context) error {
+	omniClient, err := omni.BuildClient(p.options.OmniAPIEndpoint, p.options.InsecureSkipTLSVerify)
+	if err != nil {
+		return fmt.Errorf("failed to build omni client: %w", err)
+	}
+
+	defer omniClient.Close() //nolint:errcheck
+
+	if err = omniClient.EnsureProviderStatus(ctx, p.options.Name, p.options.Description, icon); err != nil {
+		return fmt.Errorf("failed to create/update provider status: %w", err)
+	}
+
+	imageFactoryClient, err := imagefactory.NewClient(p.options.ImageFactoryBaseURL, p.options.ImageFactoryPXEBaseURL,
+		p.options.ImageFactoryTalosVersion, p.options.APIAdvertiseAddress, p.options.APIPort, p.options.MachineLabels)
+	if err != nil {
+		return fmt.Errorf("failed to create image factory client: %w", err)
+	}
+
+	ipxeHandler, err := ipxe.NewHandler(imageFactoryClient, omniClient, p.options.APIAdvertiseAddress, p.options.APIPort,
+		p.options.AgentTalosImage, p.logger.With(zap.String("component", "ipxe_handler")))
+	if err != nil {
+		return fmt.Errorf("failed to create iPXE handler: %w", err)
+	}
+
+	configHandler, err := config.NewHandler(ctx, omniClient, p.logger.With(zap.String("component", "config_handler")))
+	if err != nil {
+		return fmt.Errorf("failed to create config handler: %w", err)
+	}
+
+	srvr := server.New(ctx, p.options.APIListenAddress, p.options.APIPort, configHandler, ipxeHandler, p.logger.With(zap.String("component", "server")))
+	agentController := agent.NewController(srvr, p.logger.With(zap.String("component", "controller"))) //nolint:contextcheck // false positive
+	dhcpProxy := dhcp.NewProxy(p.options.APIAdvertiseAddress, p.options.APIPort, p.logger.With(zap.String("component", "dhcp_proxy")))
+	tftpServer := tftp.NewServer(p.logger.With(zap.String("component", "tftp_server")))
+	powerManager := power.NewManager(omniClient)
+
+	_ = agentController // todo: to be used in the new controller
+
+	eg, ctx := errgroup.WithContext(ctx)
+
+	eg.Go(p.runComponent("server", func() error {
+		return srvr.Run(ctx)
+	}))
+
+	eg.Go(p.runComponent("reverse tunnel", func() error {
+		return omniClient.RunReverseTunnel(ctx, powerManager, p.logger.With(zap.String("component", "reverse_tunnel")))
+	}))
+
+	eg.Go(p.runComponent("DHCP proxy", func() error {
+		return dhcpProxy.Run(ctx)
+	}))
+
+	eg.Go(p.runComponent("TFTP server", func() error {
+		return tftpServer.Run(ctx)
+	}))
+
+	if err = eg.Wait(); err != nil {
+		return fmt.Errorf("failed to run provider: %w", err)
+	}
+
+	return nil
+}
+
+func (p *Provider) runComponent(name string, f func() error) func() error {
+	return func() error {
+		p.logger.Info("start component ", zap.String("name", name))
+
+		err := f()
+		if err != nil {
+			p.logger.Error("failed to run component", zap.String("name", name), zap.Error(err))
+
+			return err
+		}
+
+		p.logger.Info("component stopped", zap.String("name", name))
+
+		return nil
+	}
+}
diff --git a/internal/resources/machine.go b/internal/resources/machine.go
new file mode 100644
index 0000000..ae95b53
--- /dev/null
+++ b/internal/resources/machine.go
@@ -0,0 +1,49 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package resources contains resources stored in the KubeVirt infra provider state.
+package resources
+
+import (
+	"github.com/cosi-project/runtime/pkg/resource"
+	"github.com/cosi-project/runtime/pkg/resource/meta"
+	"github.com/cosi-project/runtime/pkg/resource/protobuf"
+	"github.com/cosi-project/runtime/pkg/resource/typed"
+	"github.com/siderolabs/omni/client/pkg/infra"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/specs"
+	providermeta "github.com/siderolabs/omni-infra-provider-bare-metal/internal/meta"
+)
+
+var namespace = infra.ResourceNamespace(providermeta.ProviderID)
+
+// NewMachine creates new Machine.
+func NewMachine(id string) *Machine {
+	return typed.NewResource[MachineSpec, MachineExtension](
+		resource.NewMetadata(namespace, MachineType, id, resource.VersionUndefined),
+		protobuf.NewResourceSpec(&specs.MachineSpec{}),
+	)
+}
+
+// MachineType is the type of Machine resource.
+var MachineType = infra.ResourceType("Machine", providermeta.ProviderID)
+
+// Machine describes fake machine configuration.
+type Machine = typed.Resource[MachineSpec, MachineExtension]
+
+// MachineSpec wraps specs.MachineSpec.
+type MachineSpec = protobuf.ResourceSpec[specs.MachineSpec, *specs.MachineSpec]
+
+// MachineExtension providers auxiliary methods for Machine resource.
+type MachineExtension struct{}
+
+// ResourceDefinition implements [typed.Extension] interface.
+func (MachineExtension) ResourceDefinition() meta.ResourceDefinitionSpec {
+	return meta.ResourceDefinitionSpec{
+		Type:             MachineType,
+		Aliases:          []resource.Type{},
+		DefaultNamespace: namespace,
+		PrintColumns:     []meta.PrintColumn{},
+	}
+}
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000..3c5c1fc
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,132 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package server implements the HTTP and GRPC servers.
+package server
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/recovery"
+	"go.uber.org/zap"
+	"golang.org/x/net/http2"
+	"golang.org/x/net/http2/h2c"
+	"golang.org/x/sync/errgroup"
+	"google.golang.org/grpc"
+	"google.golang.org/grpc/codes"
+	"google.golang.org/grpc/credentials/insecure"
+	"google.golang.org/grpc/status"
+)
+
+// Server represents the HTTP and GRPC servers.
+type Server struct {
+	grpcServer *grpc.Server
+	httpServer *http.Server
+}
+
+// RegisterService registers a service with the GRPC server.
+//
+// Implements grpc.ServiceRegistrar interface.
+func (s *Server) RegisterService(desc *grpc.ServiceDesc, impl any) {
+	s.grpcServer.RegisterService(desc, impl)
+}
+
+// New creates a new server.
+func New(ctx context.Context, endpoint string, port int, configHandler, ipxeHandler http.Handler, logger *zap.Logger) *Server {
+	recoveryOption := recovery.WithRecoveryHandler(recoveryHandler(logger))
+
+	grpcServer := grpc.NewServer(
+		grpc.ChainUnaryInterceptor(recovery.UnaryServerInterceptor(recoveryOption)),
+		grpc.ChainStreamInterceptor(recovery.StreamServerInterceptor(recoveryOption)),
+		grpc.Creds(insecure.NewCredentials()),
+	)
+
+	httpServer := &http.Server{
+		Addr:    net.JoinHostPort(endpoint, strconv.Itoa(port)),
+		Handler: newMultiHandler(configHandler, ipxeHandler, grpcServer),
+		BaseContext: func(net.Listener) context.Context {
+			return ctx
+		},
+	}
+
+	return &Server{
+		grpcServer: grpcServer,
+		httpServer: httpServer,
+	}
+}
+
+// Run runs the server.
+func (s *Server) Run(ctx context.Context) error {
+	eg, ctx := errgroup.WithContext(ctx)
+
+	eg.Go(func() error {
+		<-ctx.Done()
+
+		shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+		defer cancel()
+
+		if err := s.httpServer.Shutdown(shutdownCtx); err != nil { //nolint:contextcheck
+			return fmt.Errorf("failed to shutdown iPXE server: %w", err)
+		}
+
+		return nil
+	})
+
+	eg.Go(func() error {
+		if err := s.httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
+			return fmt.Errorf("failed to run server: %w", err)
+		}
+
+		return nil
+	})
+
+	return eg.Wait()
+}
+
+func newMultiHandler(configHandler, ipxeHandler http.Handler, grpcHandler http.Handler) http.Handler {
+	mux := http.NewServeMux()
+
+	mux.Handle("/config", configHandler)
+	mux.Handle("/ipxe", ipxeHandler)
+
+	multi := &multiHandler{
+		httpHandler: mux,
+		grpcHandler: grpcHandler,
+	}
+
+	return h2c.NewHandler(multi, &http2.Server{})
+}
+
+type multiHandler struct {
+	httpHandler http.Handler
+	grpcHandler http.Handler
+}
+
+func (m *multiHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+	if req.ProtoMajor == 2 && strings.HasPrefix(
+		req.Header.Get("Content-Type"), "application/grpc") {
+		m.grpcHandler.ServeHTTP(w, req)
+
+		return
+	}
+
+	m.httpHandler.ServeHTTP(w, req)
+}
+
+func recoveryHandler(logger *zap.Logger) recovery.RecoveryHandlerFunc {
+	return func(p any) error {
+		if logger != nil {
+			logger.Error("grpc panic", zap.Any("panic", p), zap.Stack("stack"))
+		}
+
+		return status.Errorf(codes.Internal, "%v", p)
+	}
+}
diff --git a/internal/service/service.go b/internal/service/service.go
new file mode 100644
index 0000000..8396d1b
--- /dev/null
+++ b/internal/service/service.go
@@ -0,0 +1,53 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package service implements the bare metal infra provider GRPC service server.
+package service
+
+import (
+	"context"
+
+	"go.uber.org/zap"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/provider"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/api/specs"
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/resources"
+)
+
+// OmniClient is the interface to manage persisted resources.Machine resources.
+type OmniClient interface {
+	GetMachine(ctx context.Context, id string) (*resources.Machine, error)
+	SaveMachine(ctx context.Context, id string, spec *specs.MachineSpec) (*resources.Machine, error)
+	RemoveMachine(ctx context.Context, id string) error
+}
+
+// PowerManager is the interface to send power management commands to machines.
+type PowerManager interface {
+	Reboot(ctx context.Context, id string) error
+}
+
+// ProviderServiceServer is the bare metal infra provider service server.
+type ProviderServiceServer struct {
+	providerpb.UnimplementedProviderServiceServer
+
+	logger       *zap.Logger
+	powerManager PowerManager
+}
+
+// NewProviderServiceServer creates a new ProviderServiceServer.
+func NewProviderServiceServer(powerManager PowerManager, logger *zap.Logger) *ProviderServiceServer {
+	return &ProviderServiceServer{
+		powerManager: powerManager,
+		logger:       logger,
+	}
+}
+
+// RebootMachine reboots a machine.
+func (p *ProviderServiceServer) RebootMachine(ctx context.Context, request *providerpb.RebootMachineRequest) (*providerpb.RebootMachineResponse, error) {
+	if err := p.powerManager.Reboot(ctx, request.Id); err != nil {
+		return nil, err
+	}
+
+	return &providerpb.RebootMachineResponse{}, nil
+}
diff --git a/internal/tftp/tftp_server.go b/internal/tftp/tftp_server.go
new file mode 100644
index 0000000..eaf43fa
--- /dev/null
+++ b/internal/tftp/tftp_server.go
@@ -0,0 +1,124 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// Package tftp implements a TFTP server.
+package tftp
+
+import (
+	"context"
+	"io"
+	"os"
+	"path/filepath"
+	"time"
+
+	"github.com/pin/tftp/v3"
+	"go.uber.org/zap"
+	"golang.org/x/sync/errgroup"
+
+	"github.com/siderolabs/omni-infra-provider-bare-metal/internal/constants"
+)
+
+// Server represents the TFTP server serving iPXE binaries.
+type Server struct {
+	logger *zap.Logger
+}
+
+// NewServer creates a new TFTP server.
+func NewServer(logger *zap.Logger) *Server {
+	return &Server{
+		logger: logger,
+	}
+}
+
+// Run runs the TFTP server.
+func (s *Server) Run(ctx context.Context) error {
+	if err := os.MkdirAll(constants.TFTPPath, 0o777); err != nil {
+		return err
+	}
+
+	readHandler := func(filename string, rf io.ReaderFrom) error {
+		return handleRead(filename, rf, s.logger)
+	}
+
+	srv := tftp.NewServer(readHandler, nil)
+
+	// A standard TFTP server implementation receives requests on port 69 and
+	// allocates a new high port (over 1024) dedicated to that request. In single
+	// port mode, the same port is used for transmit and receive. If the server
+	// is started on port 69, all communication will be done on port 69.
+	// This option is required since the Kubernetes service definition defines a
+	// single port.
+	srv.EnableSinglePort()
+	srv.SetTimeout(5 * time.Second)
+
+	eg, ctx := errgroup.WithContext(ctx)
+
+	eg.Go(func() error {
+		return srv.ListenAndServe(":69")
+	})
+
+	eg.Go(func() error {
+		<-ctx.Done()
+
+		srv.Shutdown()
+
+		return nil
+	})
+
+	return eg.Wait()
+}
+
+// cleanPath makes a path safe for use with filepath.Join. This is done by not
+// only cleaning the path, but also (if the path is relative) adding a leading
+// '/' and cleaning it (then removing the leading '/'). This ensures that a
+// path resulting from prepending another path will always resolve to lexically
+// be a subdirectory of the prefixed path. This is all done lexically, so paths
+// that include symlinks won't be safe as a result of using CleanPath.
+func cleanPath(path string) string {
+	// Deal with empty strings nicely.
+	if path == "" {
+		return ""
+	}
+
+	// Ensure that all paths are cleaned (especially problematic ones like
+	// "/../../../../../" which can cause lots of issues).
+	path = filepath.Clean(path)
+
+	// If the path isn't absolute, we need to do more processing to fix paths
+	// such as "../../../../<etc>/some/path". We also shouldn't convert absolute
+	// paths to relative ones.
+	if !filepath.IsAbs(path) {
+		path = filepath.Clean(string(os.PathSeparator) + path)
+		// This can't fail, as (by definition) all paths are relative to root.
+		path, _ = filepath.Rel(string(os.PathSeparator), path) //nolint:errcheck
+	}
+
+	// Clean the path again for good measure.
+	return filepath.Clean(path)
+}
+
+// handleRead is called when a client starts file download from server.
+func handleRead(filename string, rf io.ReaderFrom, logger *zap.Logger) error {
+	filename = filepath.Join(constants.TFTPPath, cleanPath(filename))
+
+	file, err := os.Open(filename)
+	if err != nil {
+		logger.Error("failed to open file", zap.String("filename", filename), zap.Error(err))
+
+		return err
+	}
+
+	defer file.Close() //nolint:errcheck
+
+	n, err := rf.ReadFrom(file)
+	if err != nil {
+		logger.Error("failed to read from file", zap.String("filename", filename), zap.Error(err))
+
+		return err
+	}
+
+	logger.Info("file sent", zap.String("filename", filename), zap.Int64("bytes", n))
+
+	return nil
+}
diff --git a/internal/version/data/sha b/internal/version/data/sha
new file mode 100644
index 0000000..66dc905
--- /dev/null
+++ b/internal/version/data/sha
@@ -0,0 +1 @@
+undefined
\ No newline at end of file
diff --git a/internal/version/data/tag b/internal/version/data/tag
new file mode 100644
index 0000000..66dc905
--- /dev/null
+++ b/internal/version/data/tag
@@ -0,0 +1 @@
+undefined
\ No newline at end of file
diff --git a/internal/version/version.go b/internal/version/version.go
new file mode 100644
index 0000000..b39b93f
--- /dev/null
+++ b/internal/version/version.go
@@ -0,0 +1,41 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+// THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT.
+//
+// Generated on 2024-10-14T09:32:55Z by kres 34e72ac.
+
+// Package version contains variables such as project name, tag and sha. It's a proper alternative to using
+// -ldflags '-X ...'.
+package version
+
+import (
+	_ "embed"
+	"runtime/debug"
+	"strings"
+)
+
+var (
+	// Tag declares project git tag.
+	//go:embed data/tag
+	Tag string
+	// SHA declares project git SHA.
+	//go:embed data/sha
+	SHA string
+	// Name declares project name.
+	Name = func() string {
+		info, ok := debug.ReadBuildInfo()
+		if !ok {
+			panic("cannot read build info, something is very wrong")
+		}
+
+		// Check if siderolabs project
+		if strings.HasPrefix(info.Path, "github.com/siderolabs/") {
+			return info.Path[strings.LastIndex(info.Path, "/")+1:]
+		}
+
+		// We could return a proper full path here, but it could be seen as a privacy violation.
+		return "community-project"
+	}()
+)