diff --git a/.env.workshop b/.env.workshop new file mode 100644 index 0000000..06c241e --- /dev/null +++ b/.env.workshop @@ -0,0 +1,2 @@ +export COMPOSE_PROFILES=lso +export LSO_ENABLED=True diff --git a/.gitignore b/.gitignore index de0f2db..2da31ef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,6 @@ lint.sh clab-* *.clab.yml.bak .env + +# Artifacts from the e2e tests +screenshots/ diff --git a/README.md b/README.md index 50fb33a..ef34636 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,62 @@ Example workflow orchestrator implementation based on the ## Quickstart +### Docker runtime on Apple Silicon (colima + virtiofs) + +> [!NOTE] +> This section is **only** needed on **ARM / Apple Silicon (M-series) Macs**. On +> Linux and Intel machines the stock Docker install works and you can skip ahead +> to [Start application](#start-application). + +The containerlab SR Linux nodes (`clab/srlinux01.clab.yaml`) don't run under a +default macOS Docker setup. They need a Docker VM that mounts host paths with +**virtiofs** and has enough memory. Two hard requirements, both learned the hard +way: + +- **`virtiofs` mounts, not `sshfs`.** SR Linux generates a TLS keypair at boot and + `chown`s it. `sshfs` (colima's legacy mount type) forbids `chown` even as root, + so the keygen fails, `sr_linux_mgr` crash-loops, and no management server + (JSON-RPC / gNMI / gRPC) ever comes online. +- **≥ 8 GB VM memory.** Each SR Linux node reserves ~2 GB. On a smaller VM the + kernel OOM-killer repeatedly kills `sr_mgmt_server` and the nodes never settle. + +Install [colima](https://github.com/abiosoft/colima) and the Docker CLI, then +create the VM with the right settings: + +```bash +brew install colima docker + +# vm-type vz + virtiofs is the Apple Silicon fast/reliable path. +# Sizes: 8 GB RAM / 4 CPUs comfortably runs the 3-node lab + the compose stack. +colima start --vm-type vz --mount-type virtiofs --memory 8 --cpu 4 +``` + +> [!CAUTION] +> **Do not run Docker Desktop at the same time as colima.** Both register a +> `docker` CLI context and fight over the `/var/run/docker.sock` symlink and host +> ports, giving you a broken, non-deterministic Docker setup. Quit Docker Desktop +> (and disable "Start at login") before using colima. + +> [!WARNING] +> `mountType` and `vmType` can **only** be set when the VM is first created — the +> `--mount-type`/`--vm-type` flags are silently ignored on an existing VM. If your +> colima VM was created with `sshfs`, recreate it (this destroys the VM's +> containers, images and volumes): +> +> ```bash +> colima delete && colima start --vm-type vz --mount-type virtiofs --memory 8 --cpu 4 +> ``` +> +> Memory and CPU, by contrast, *can* be changed on a plain restart: +> `colima stop && colima start --memory 8 --cpu 4`. + +Verify the VM before continuing: + +```bash +colima status | grep mountType # -> mountType: virtiofs +colima ssh -- free -h # Mem total should be ~8 GB +``` + ### Start application Make sure you have docker installed and run: @@ -89,7 +145,7 @@ This will start the `orchestrator`, `orchestrator-ui`, `netbox`, `federation`, ` To include LSO, run the following command instead: ``` -COMPOSE_PROFILES=lso docker compose up +COMPOSE_PROFILES=lso LSO_ENABLED=True docker compose up ``` This will build the Docker image for LSO locally, and make the orchestrator use the included Ansible playbooks. @@ -422,7 +478,7 @@ are defined: - SAPBlock - VirtualCircuitBlock -Usually, the top-level product block if a product is named after the +Usually, the top-level product block of a product is named after the product, but this is not true for the top-level product block of the L2VPN product. The more generic name `VirtualCIrcuitBlock` allows the reuse of this product block by other services like Internet Access and @@ -631,7 +687,7 @@ class Port(PortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): ``` As can be seen in the above example, the inactive product type -definition is subclassed from SubScriptionModel, and the following +definition is subclassed from SubscriptionModel, and the following definitions are subclassed from the previous one. This product has one fixed input called speed and one port product block (see below about naming). Notice that the port product block matches the lifecycle of the @@ -741,9 +797,8 @@ optional to allow the creation of an empty product block instance. All resource types that are used to hold the user input for the subscription is stored using resource types that are not optional anymore in the provisioning lifecycle state. All resource types used to store -information that is generated while provisioning the subscription is -stored using resource types that are optional while provisioning but are -not optional anymore for the active lifecycle state. Resource types that +information for the provisioning lifecycle state are optional, but they aren't +optional for the active lifecycle state. Resource types that are still optional in the active state are used to store non-mandatory information. @@ -791,7 +846,7 @@ not fail. Sometimes there are resource types that depend on information stored on other product blocks, even on linked product blocks that do not belong -to the same subscription. This kind of types need to be calculated at +to the same subscription. This allows for types to be calculated at run time so that they include the most recent information. Consider the following example of a, stripped down version, of a port and node product block, and a title for the port block that is generated @@ -805,7 +860,8 @@ class PortBlock(PortBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]) port_name: str node: NodeBlock - @serializable_property + @computed_field + @property def title(self) -> str: return f"{self.port_name} on {self.node.node_name}" @@ -813,11 +869,11 @@ class Port(PortProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]): port: PortBlock ``` -A `@serializable_property` has been added that will dynamically render +A `@computed_field` has been added that will dynamically render the title of the port product block. Even after a modify workflow was run to change the node name on the node subscription, the title of the port block will always be up to date. The title can be referenced as any -other resource type using subscription.port.title. This is not a random +other resource type using `subscription.port.title`. This is not a fictional example, the title of a product block is used by the orchestrator GUI while displaying detailed subscription information. diff --git a/ansible/plays_and_roles/create_node.yaml b/ansible/plays_and_roles/create_node.yaml index 4be4da6..7cb0191 100644 --- a/ansible/plays_and_roles/create_node.yaml +++ b/ansible/plays_and_roles/create_node.yaml @@ -7,7 +7,7 @@ ansible_user: admin ansible_password: NokiaSrl1! netbox_url: "http://netbox:8080" - netbox_token: "e744057d755255a31818bf74df2350c26eeabe54" + netbox_token: "nbt_sDx3hakFeTCZ.lnGwrK42a7z1IelVaigWXqaAB0GKSpbOSZKTrLdb" debug: false tasks: - name: Dump subscripion diff --git a/clab/configs/srl-jsonrpc.partial.cli b/clab/configs/srl-jsonrpc.partial.cli new file mode 100644 index 0000000..a41d8a1 --- /dev/null +++ b/clab/configs/srl-jsonrpc.partial.cli @@ -0,0 +1,2 @@ +set / system json-rpc-server admin-state enable +set / system json-rpc-server network-instance mgmt http admin-state enable diff --git a/clab/srlinux01.clab.yaml b/clab/srlinux01.clab.yaml index 5c7f4b2..57aed0b 100644 --- a/clab/srlinux01.clab.yaml +++ b/clab/srlinux01.clab.yaml @@ -8,6 +8,8 @@ topology: srl: type: ixrd2 image: ghcr.io/nokia/srlinux:25.7.1 + # Enable JSON-RPC (HTTP) at boot so the orchestrator can reach every srl node. + startup-config: configs/srl-jsonrpc.partial.cli # cvx: # type: cumulus_cvx # image: networkop/cx:4.3.0 diff --git a/docker-compose.yml b/docker-compose.yml index f987d0c..395704e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -169,7 +169,7 @@ services: - path: ./docker/overrides/orchestrator/orchestrator.env required: false environment: - LSO_ENABLED: ${COMPOSE_PROFILES:+True} + LSO_ENABLED: ${LSO_ENABLED:-False} ports: - "8080:8080" - "5678:5678" #Enable Python debugger @@ -211,7 +211,6 @@ services: lso: container_name: orchestrator-lso - #image: orchestrator-lso profiles: - lso build: diff --git a/docker/lso/Dockerfile b/docker/lso/Dockerfile index bebe68b..94796c2 100644 --- a/docker/lso/Dockerfile +++ b/docker/lso/Dockerfile @@ -1,5 +1,8 @@ FROM python:3.13.13-alpine +# Keep in sync with lso/lso.env +ENV ANSIBLE_COLLECTIONS_PATH=/app/demo/ansible/ansible_collections + WORKDIR /app RUN apk add --update --no-cache gcc libc-dev libffi-dev diff --git a/docker/lso/lso.env b/docker/lso/lso.env index 6e417b2..7a06deb 100644 --- a/docker/lso/lso.env +++ b/docker/lso/lso.env @@ -2,6 +2,7 @@ # Ansible configuration ANSIBLE_PLAYBOOKS_ROOT_DIR="/app/demo/ansible/playbooks" +# Keep in sync with lso/Dockerfile ANSIBLE_COLLECTIONS_PATH="/app/demo/ansible/ansible_collections" ANSIBLE_HOST_KEY_CHECKING: "False"