Skip to content

Commit

Permalink
Merge pull request #27 from skrobul/workflow-create-node
Browse files Browse the repository at this point in the history
feat: Workflow to synchronize Nautobot Device to Ironic Node
  • Loading branch information
cardoe authored Apr 8, 2024
2 parents 0b8e57f + c0bcd72 commit 34eee16
Show file tree
Hide file tree
Showing 28 changed files with 1,563 additions and 0 deletions.
72 changes: 72 additions & 0 deletions .github/workflows/build-container-images.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: build-container-images

on:
pull_request:
paths:
- 'argo-workflows/generic/containers/*'
- 'argo-workflows/ironic-nautobot-sync/containers/*'
push:
branches:
- main
paths:
- 'argo-workflows/generic/containers/*'
- 'argo-workflows/ironic-nautobot-sync/containers/*'

# bump container versions here, they will be populated to tags and labels
env:
VERSION_PYTHON311: 0.0.1
VERSION_PYTHON312: 0.0.1
VERSION_PYTHON_IRONIC: 0.0.1

jobs:
build-ghcr-registry:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to ghcr.io
if: ${{ github.event_name != 'pull_request' }}
uses: docker/login-action@v3
with:
registry: "ghcr.io"
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and deploy Python 3.11 image
uses: docker/build-push-action@v5
with:
context: argo-workflows/generic/
file: argo-workflows/generic/containers/Dockerfile.python311_alpine
# push for all main branch commits
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/rackerlabs/understack/argo-python3.11.8-alpine3.19:latest,ghcr.io/rackerlabs/understack/argo-python3.11.8-alpine3.19:${{ env.VERSION_PYTHON311 }}
labels: |
org.opencontainers.image.version: "${{ env.VERSION_PYTHON311 }}"
- name: Build and deploy Python 3.12 image
uses: docker/build-push-action@v5
with:
context: argo-workflows/generic/
file: argo-workflows/generic/containers/Dockerfile.python312_alpine
# push for all main branch commits
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19:latest,ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19:${{ env.VERSION_PYTHON312 }}
labels: |
org.opencontainers.image.version: "${{ env.VERSION_PYTHON312 }}"
- name: Build and deploy Python 3.11 with Ironic client
uses: docker/build-push-action@v5
with:
context: argo-workflows/ironic-nautobot-sync
file: argo-workflows/ironic-nautobot-sync/containers/Dockerfile.ironic
# push for all main branch commits
push: ${{ github.event_name != 'pull_request' }}
tags: ghcr.io/rackerlabs/understack/argo-ironic-client-python3.11.8:latest,ghcr.io/rackerlabs/understack/argo-ironic-client-python3.11.8:${{ env.VERSION_PYTHON_IRONIC }}
labels: |
org.opencontainers.image.version: "${{ env.VERSION_PYTHON_IRONIC }}"
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
Empty file.
44 changes: 44 additions & 0 deletions argo-workflows/generic/containers/Dockerfile.python311_alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM python:3.11.8-alpine3.19 as builder

LABEL org.opencontainers.image.title="Python 3.11 image base image"
LABEL org.opencontainers.image.base.name="ghcr.io/rackerlabs/understack/argo-python3.11.8-alpine3.19"
LABEL org.opencontainers.image.source=https://github.com/rackerlabs/understack

ENV PYTHONUNBUFFERED=1

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

ARG APP_PATH=/app
ARG APP_USER=appuser
ARG APP_GROUP=appgroup
ARG APP_USER_UID=1000
ARG APP_GROUP_GID=1000

RUN addgroup -g $APP_GROUP_GID -S $APP_GROUP && \
adduser -S -s /sbin/nologin -u $APP_USER_UID -G $APP_GROUP $APP_USER && \
mkdir $APP_PATH && \
chown $APP_USER:$APP_GROUP $APP_PATH
WORKDIR /app
CMD ["python", "-"]

# Example usage in final image
# FROM ghcr.io/rackerlabs/understack/argo-python3.11.8-alpine3.19
#
# # This section needs to be repeated in child images
# ARG APP_PATH=/app
# ARG APP_USER=appuser
# ARG APP_GROUP=appgroup
# ARG APP_USER_UID=1000
# ARG APP_GROUP_GID=1000
#
#
# RUN --mount=type=cache,target=/var/cache/apk apk add --virtual build-deps gcc python3-dev musl-dev linux-headers
# RUN --mount=type=cache,target=/root/.cache/.pip pip install --no-cache-dir python-ironicclient==5.4.0
#
# FROM ghcr.io/rackerlabs/understack/argo-python3.11.8-alpine3.19 as prod
# ENV PATH="/opt/venv/bin:$PATH"
# COPY --from=builder /opt/venv /opt/venv
#
# WORKDIR /app
# CMD ["python", "-"]
44 changes: 44 additions & 0 deletions argo-workflows/generic/containers/Dockerfile.python312_alpine
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
FROM python:3.12.2-alpine3.19 as builder

LABEL org.opencontainers.image.title="Python 3.12 image base image"
LABEL org.opencontainers.image.base.name="ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19"
LABEL org.opencontainers.image.source=https://github.com/rackerlabs/understack

ENV PYTHONUNBUFFERED=1

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

ARG APP_PATH=/app
ARG APP_USER=appuser
ARG APP_GROUP=appgroup
ARG APP_USER_UID=1000
ARG APP_GROUP_GID=1000

RUN addgroup -g $APP_GROUP_GID -S $APP_GROUP && \
adduser -S -s /sbin/nologin -u $APP_USER_UID -G $APP_GROUP $APP_USER && \
mkdir $APP_PATH && \
chown $APP_USER:$APP_GROUP $APP_PATH
WORKDIR /app
CMD ["python", "-"]

# Example usage in final image
# FROM ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19
#
# # This section needs to be repeated in child images
# ARG APP_PATH=/app
# ARG APP_USER=appuser
# ARG APP_GROUP=appgroup
# ARG APP_USER_UID=1000
# ARG APP_GROUP_GID=1000
#
#
# RUN --mount=type=cache,target=/var/cache/apk apk add --virtual build-deps gcc python3-dev musl-dev linux-headers
# RUN --mount=type=cache,target=/root/.cache/.pip pip install --no-cache-dir python-ironicclient==5.4.0
#
# FROM ghcr.io/rackerlabs/understack/argo-python3.12.2-alpine3.19 as prod
# ENV PATH="/opt/venv/bin:$PATH"
# COPY --from=builder /opt/venv /opt/venv
#
# WORKDIR /app
# CMD ["python", "-"]
Empty file.
25 changes: 25 additions & 0 deletions argo-workflows/ironic-nautobot-sync/code/create_node.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import json
import logging
import sys

from ironic.client import IronicClient
from ironic.secrets import read_secret

logger = logging.getLogger(__name__)


if len(sys.argv) < 1:
raise ValueError("Please provide node configuration in JSON format as first argument.")

logger.info("Pushing device new node to Ironic.")
client = IronicClient(
svc_url=read_secret("IRONIC_SVC_URL"),
username=read_secret("IRONIC_USERNAME"),
password=read_secret("IRONIC_PASSWORD"),
auth_url=read_secret("IRONIC_AUTH_URL"),
tenant_name=read_secret("IRONIC_TENANT"),
)

node_config = json.loads(sys.argv[1])
response = client.create_node(node_config)
logger.debug(response)
69 changes: 69 additions & 0 deletions argo-workflows/ironic-nautobot-sync/code/ironic/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
from ironicclient import client as iclient
from keystoneauth1 import session
from keystoneauth1.identity import v3


class IronicClient:
def __init__(
self,
svc_url: str,
username: str,
password: str,
auth_url: str,
tenant_name: str,
) -> None:
self.svc_url = svc_url
self.username = username
self.password = password
self.auth_url = auth_url
self.tenant_name = tenant_name
self.logged_in = False
self.os_ironic_api_version = "1.82"

def login(self):
auth = v3.Password(
auth_url=self.auth_url,
username=self.username,
password=self.password,
project_name=self.tenant_name,
project_domain_name="Default",
user_domain_name="Default",
)
insecure_ssl = True
sess = session.Session(
auth=auth, verify=(not insecure_ssl), app_name="nautobot"
)
self.client = iclient.Client(
1,
endpoint_override=self.svc_url,
session=sess,
insecure=insecure_ssl,
)
self.client.negotiate_api_version()
self.logged_in = True

def create_node(self, node_data: dict):
self._ensure_logged_in()

return self.client.node.create(
os_ironic_api_version=self.os_ironic_api_version, **node_data
)

def list_nodes(self):
self._ensure_logged_in()

return self.client.node.list()

def get_node(self, node_ident: str, fields: list[str] | None = None):
self._ensure_logged_in()

return self.client.node.get(node_ident, fields, os_ironic_api_version=self.os_ironic_api_version)

def update_node(self, node_id, patch):
self._ensure_logged_in()

return self.client.node.update(node_id, patch, os_ironic_api_version=self.os_ironic_api_version)

def _ensure_logged_in(self):
if not self.logged_in:
self.login()
19 changes: 19 additions & 0 deletions argo-workflows/ironic-nautobot-sync/code/ironic/secrets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import os
import re
import logging

logger = logging.getLogger(__name__)

def read_secret(secret_name: str) -> str:
"""Retrieve value of Kubernetes secret"""
def normalized(name):
return re.sub(r'[^A-Za-z0-9-_]', '', name)

base_path = os.environ.get('SECRETS_BASE_PATH', '/etc/ironic-secrets/')
secret_path = os.path.join(base_path, normalized(secret_name))
try:
return open(secret_path, "r").read()
except FileNotFoundError:
logger.error(f"Secret {secret_name} is not defined.")
return ""

55 changes: 55 additions & 0 deletions argo-workflows/ironic-nautobot-sync/code/network_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from dataclasses import dataclass
from dataclasses import field


@dataclass
class PhysicalNic:
id: str
ethernet_mac_address: str
type: str = "phy"
mtu: int = 1500


@dataclass
class NetworkRoute:
network: str
netmask: str
gateway: str


@dataclass
class BondedNic:
id: str
link: str
ethernet_mac_address: str
mtu: int = 1500
type: str = "bond"
bond_links: list[str] = field(default_factory=list)
bond_mode: str = "802.1ad"
bond_xmit_hash_policy: str = "layer3+4"
bond_miimon: int | None = 100
routes: list[NetworkRoute] = field(default_factory=list)


@dataclass
class Network:
id: str | None = None
network_id: str | None = None
link: str | None = None
type: str = "ipv4"
ip_address: str | None = None
netmask: str | None = None
routes: list[NetworkRoute] = field(default_factory=list)


@dataclass
class Service:
type: str
address: str


@dataclass
class NetworkInfo:
links: list[PhysicalNic | BondedNic] = field(default_factory=list)
networks: list[Network] = field(default_factory=list)
services: list[Service] = field(default_factory=list)
Loading

0 comments on commit 34eee16

Please sign in to comment.