diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 2a25d23f5..75ed47275 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -322,6 +322,14 @@ jobs: with: py_ver: ${{ needs.file-changes.outputs.py_ver }} + fortigate-tests: + uses: ./.github/workflows/fortigate-tests.yml + needs: + - file-changes + - build-containerlab + with: + py_ver: ${{ needs.file-changes.outputs.py_ver }} + # a job that downloads coverage artifact and uses codecov to upload it coverage: runs-on: ubuntu-22.04 @@ -335,6 +343,7 @@ jobs: - vxlan-tests - kind-tests - sros-tests + - fortigate-tests steps: - name: Checkout diff --git a/.github/workflows/fortigate-tests.yml b/.github/workflows/fortigate-tests.yml new file mode 100644 index 000000000..5489ee170 --- /dev/null +++ b/.github/workflows/fortigate-tests.yml @@ -0,0 +1,71 @@ +name: fortigate-test + +"on": + workflow_call: + inputs: + py_ver: + required: true + type: string + +jobs: + fortigate-tests: + runs-on: ubuntu-22.04 + strategy: + matrix: + runtime: + - "docker" + test-suite: + - "01*.robot" + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v4 + with: + name: containerlab + + - name: Move containerlab to usr/bin + run: sudo mv ./containerlab /usr/bin/containerlab && sudo chmod a+x /usr/bin/containerlab + + - uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.py_ver }} + cache: pip + cache-dependency-path: "tests/requirements.txt" + + - name: Install robotframework + run: | + pip install -r tests/requirements.txt + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Sanitize test-suite name + run: echo "TEST_SUITE=$(echo ${{ matrix.test-suite }} | tr -d '*')" >> $GITHUB_ENV + + - name: Run tests + run: | + bash ./tests/rf-run.sh ${{ matrix.runtime }} ./tests/09-fortigate/${{ matrix.test-suite }} + + # upload test reports as a zip file + - uses: actions/upload-artifact@v4 + if: always() + with: + name: 09-fortigate-log-${{ env.TEST_SUITE }}-${{ matrix.runtime }} + path: ./tests/out/*.html + + # upload coverage report from unit tests, as they are then + # merged with e2e tests coverage + - name: Upload coverage report + uses: actions/upload-artifact@v4 + if: always() + with: + name: coverage-fortigate-tests-${{ env.TEST_SUITE }}-${{ matrix.runtime }} + path: /tmp/clab-tests/coverage/* + retention-days: 7 diff --git a/README.md b/README.md index ea932a9bd..aaaf5ca0b 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ In addition to native containerized NOSes, containerlab can launch traditional v * [Palo Alto PAN](https://containerlab.dev/manual/kinds/vr-pan) * [IPInfusion OcNOS](https://containerlab.dev/manual/kinds/ipinfusion-ocnos) * [Check Point Cloudguard](https://containerlab.dev/manual/kinds/checkpoint_cloudguard/) +* [Fortinet Fortigate](https://containerlab.dev/manual/kinds/fortinet_fortigate/) * [Aruba AOS-CX](https://containerlab.dev/manual/kinds/vr-aoscx) * [OpenBSD](https://containerlab.dev/manual/kinds/openbsd) diff --git a/clab/register.go b/clab/register.go index ed1f7358b..85123204d 100644 --- a/clab/register.go +++ b/clab/register.go @@ -13,6 +13,7 @@ import ( crpd "github.com/srl-labs/containerlab/nodes/crpd" cvx "github.com/srl-labs/containerlab/nodes/cvx" ext_container "github.com/srl-labs/containerlab/nodes/ext_container" + fortinet_fortigate "github.com/srl-labs/containerlab/nodes/fortinet_fortigate" host "github.com/srl-labs/containerlab/nodes/host" ipinfusion_ocnos "github.com/srl-labs/containerlab/nodes/ipinfusion_ocnos" k8s_kind "github.com/srl-labs/containerlab/nodes/k8s_kind" @@ -50,6 +51,7 @@ func (c *CLab) RegisterNodes() { crpd.Register(c.Reg) cvx.Register(c.Reg) ext_container.Register(c.Reg) + fortinet_fortigate.Register(c.Reg) host.Register(c.Reg) ipinfusion_ocnos.Register(c.Reg) keysight_ixiacone.Register(c.Reg) diff --git a/docs/index.md b/docs/index.md index 0b89cbc03..3b2932d13 100644 --- a/docs/index.md +++ b/docs/index.md @@ -47,6 +47,7 @@ In addition to native containerized NOSes, containerlab can launch traditional v * [Palo Alto PAN](manual/kinds/vr-pan.md) * [IPInfusion OcNOS](manual/kinds/ipinfusion-ocnos.md) * [Check Point Cloudguard](manual/kinds/checkpoint_cloudguard.md) +* [Fortinet Fortigate](manual/kinds/fortinet_fortigate.md) * [Aruba AOS-CX](manual/kinds/vr-aoscx.md) * [OpenBSD](manual/kinds/openbsd.md) diff --git a/docs/manual/kinds/fortinet_fortigate.md b/docs/manual/kinds/fortinet_fortigate.md new file mode 100644 index 000000000..ca1a5dbc8 --- /dev/null +++ b/docs/manual/kinds/fortinet_fortigate.md @@ -0,0 +1,63 @@ +--- +search: + boost: 4 +--- +# Fortinet Fortigate + +Fortinet Fortigate virtualized security appliance is identified with the `fortinet_fortigate` kind in the [topology file](../topo-def-file.md). It is built using the [hellt/vrnetlab](../vrnetlab.md) project and essentially is a Qemu VM packaged in a docker container format. + +## Getting Fortinet Fortigate disk image + +Users can obtain the qcow2 disk image for Fortinet Fortigate VM from the [official support site](https://support.fortinet.com/Download/VMImages.aspx); a free account required. Download the "New deployment" variant of the FGVM64 VM for the KVM platform. + +Extract the downloaded zip file and rename the `fortios.qcow2` to `fortios-vX.Y.Z.qcow2` where `X.Y.Z` is the version of the Fortigate VM. Put the renamed file in the `fortigate` directory of the cloned [hellt/vrnetlab](https://github.com/hellt/vrnetlab) project and run `make` to build the container image. + +## Managing Fortinet Fortigate nodes + +/// note +Containers with Fortinet Fortigate VM inside will take ~2min to fully boot. +You can monitor the progress with the `docker logs -f ` command. +/// + +Fortinet Fortigate node launched with containerlab can be managed via the following interfaces: + +/// tab | bash +to connect to a `bash` shell of a running fortigate container: + +```bash +docker exec -it bash +``` + +/// +/// tab | CLI +to connect to the Fortigate CLI + +```bash +ssh admin@ +``` + +/// +/// tab | Web UI (HTTP) +Fortigate VM comes with HTTP(S) server with a GUI manager app. You can access the Web UI using http schema. + +```bash +http:// +``` + +You can expose container's port 80 with the [`ports`](../nodes.md#ports) setting in containerlab and get access to the Web UI using your containerlab host IP. +/// +/// note +Default login credentials: `admin:admin` +/// + +## Interfaces mapping + +Fortinet Fortigate interfaces are named as follows in the topology file: + +* `eth0` - management interface connected to the containerlab management network (rendered as `port1` in the CLI) +* `eth1` - first data interface, mapped to the first data port of the VM (rendered as `port2`) +* `eth2+` - second and subsequent data interface + +When containerlab launches Fortigate node the `port1` interface of the VM gets assigned `10.0.0.15/24` address from the QEMU DHCP server. This interface is transparently stitched with container's `eth0` interface such that users can reach the management plane of the Fortigate using containerlab's assigned IP. + +Data interfaces `eth1+` need to be configured with IP addressing manually using CLI or other available management interfaces. diff --git a/docs/manual/kinds/index.md b/docs/manual/kinds/index.md index c02692b1c..b09c805a2 100644 --- a/docs/manual/kinds/index.md +++ b/docs/manual/kinds/index.md @@ -59,6 +59,7 @@ Within each predefined kind, we store the necessary information that is used to | **OpenBSD** | [`openbsd`](openbsd.md) | supported | VM | | **Keysight ixia-c-one** | [`keysight_ixia-c-one`](keysight_ixia-c-one.md) | supported | container | | **Check Point Cloudguard** | [`checkpoint_cloudguard`](checkpoint_cloudguard.md) | supported | VM | +| **Fortinet Fortigate** | [`fortinet_fortigate`](fortinet_fortigate.md) | supported | VM | | **Palo Alto PAN** | [`vr-panos/vr-paloalto_panos`](vr-pan.md) | supported | VM | | **Linux bridge** | [`bridge`](bridge.md) | supported | N/A | | **Linux container** | [`linux`](linux.md) | supported | container | diff --git a/docs/manual/vrnetlab.md b/docs/manual/vrnetlab.md index 8eabb85a9..1e0c42a09 100644 --- a/docs/manual/vrnetlab.md +++ b/docs/manual/vrnetlab.md @@ -104,6 +104,7 @@ The images that work with containerlab will appear in the supported list as we i | Aruba AOS-CX | [aruba_aoscx](kinds/vr-aoscx.md) | | | | IPInfusion OcNOS | [ipinfusion_ocnos](kinds/ipinfusion-ocnos.md) | | | | Checkpoint Cloudguard | [checkpoint_cloudguard](kinds/checkpoint_cloudguard.md) | | | +| Fortinet Fortigate | [fortinet_fortigate](kinds/fortinet_fortigate.md) | | | ### Connection modes diff --git a/lab-examples/fortigate/fortigate.clab.yml b/lab-examples/fortigate/fortigate.clab.yml new file mode 100644 index 000000000..d7d6e6ada --- /dev/null +++ b/lab-examples/fortigate/fortigate.clab.yml @@ -0,0 +1,11 @@ +name: fortigate +topology: + nodes: + forti1: + kind: fortinet_fortigate + image: vrnetlab/vr-fortigate:7.0.14 + forti2: + kind: fortinet_fortigate + image: vrnetlab/vr-fortigate:7.0.14 + links: + - endpoints: ["forti1:eth1", "forti2:eth1"] diff --git a/mkdocs.yml b/mkdocs.yml index f5a1fa71f..84b8d8ded 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -35,6 +35,7 @@ nav: - OpenBSD: manual/kinds/openbsd.md - Keysight IXIA-C One: manual/kinds/keysight_ixia-c-one.md - Check Point Cloudguard: manual/kinds/checkpoint_cloudguard.md + - Fortinet Fortigate: manual/kinds/fortinet_fortigate.md - Palo Alto PAN: manual/kinds/vr-pan.md - KinD: manual/kinds/k8s-kind.md - Linux bridge: manual/kinds/bridge.md diff --git a/nodes/fortinet_fortigate/fortigate.go b/nodes/fortinet_fortigate/fortigate.go new file mode 100644 index 000000000..9cc514ec2 --- /dev/null +++ b/nodes/fortinet_fortigate/fortigate.go @@ -0,0 +1,90 @@ +// Copyright 2020 Nokia +// Licensed under the BSD 3-Clause License. +// SPDX-License-Identifier: BSD-3-Clause + +package fortinet_fortigate + +import ( + "context" + "fmt" + "path" + + "github.com/srl-labs/containerlab/nodes" + "github.com/srl-labs/containerlab/types" + "github.com/srl-labs/containerlab/utils" +) + +var ( + kindnames = []string{"fortinet_fortigate"} + defaultCredentials = nodes.NewCredentials("admin", "admin") +) + +const ( + // scrapligo doesn't have a driver for fortigate, can be copied from scrapli community + // scrapliPlatformName = "fortinet_fortigate" + configDirName = "config" + startupCfgFName = "startup-config.cfg" +) + +// Register registers the node in the NodeRegistry. +func Register(r *nodes.NodeRegistry) { + r.Register(kindnames, func() nodes.Node { + return new(fortigate) + }, defaultCredentials) +} + +type fortigate struct { + nodes.DefaultNode +} + +func (n *fortigate) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error { + // Init DefaultNode + n.DefaultNode = *nodes.NewDefaultNode(n) + // set virtualization requirement + n.HostRequirements.VirtRequired = true + + n.Cfg = cfg + for _, o := range opts { + o(n) + } + // env vars are used to set launch.py arguments in vrnetlab container + defEnv := map[string]string{ + "USERNAME": defaultCredentials.GetUsername(), + "PASSWORD": defaultCredentials.GetPassword(), + "CONNECTION_MODE": nodes.VrDefConnMode, + "VCPU": "2", + "RAM": "2048", + "DOCKER_NET_V4_ADDR": n.Mgmt.IPv4Subnet, + "DOCKER_NET_V6_ADDR": n.Mgmt.IPv6Subnet, + } + + n.Cfg.Env = utils.MergeStringMaps(defEnv, n.Cfg.Env) + + // mount config dir to support startup-config functionality + n.Cfg.Binds = append(n.Cfg.Binds, fmt.Sprint(path.Join(n.Cfg.LabDir, configDirName), ":/config")) + + if n.Cfg.Env["CONNECTION_MODE"] == "macvtap" { + // mount dev dir to enable macvtap + n.Cfg.Binds = append(n.Cfg.Binds, "/dev:/dev") + } + + n.Cfg.Cmd = fmt.Sprintf("--username %s --password %s --hostname %s --connection-mode %s --trace", + defaultCredentials.GetUsername(), defaultCredentials.GetPassword(), n.Cfg.ShortName, n.Cfg.Env["CONNECTION_MODE"]) + + return nil +} + +func (n *fortigate) PreDeploy(_ context.Context, params *nodes.PreDeployParams) error { + utils.CreateDirectory(n.Cfg.LabDir, 0777) + _, err := n.LoadOrGenerateCertificate(params.Cert, params.TopologyName) + if err != nil { + return nil + } + + return nodes.LoadStartupConfigFileVr(n, configDirName, startupCfgFName) +} + +// CheckInterfaceName checks if a name of the interface referenced in the topology file correct. +func (n *fortigate) CheckInterfaceName() error { + return nodes.GenericVMInterfaceCheck(n.Cfg.ShortName, n.Endpoints) +} diff --git a/schemas/clab.schema.json b/schemas/clab.schema.json index 06f7116cf..f7add9f32 100644 --- a/schemas/clab.schema.json +++ b/schemas/clab.schema.json @@ -98,7 +98,8 @@ "cisco_c8000", "cvx", "cumulus_cvx", - "openbsd" + "openbsd", + "fortinet_fortigate" ] }, "license": { diff --git a/tests/09-fortigate/01-two-fortigates.robot b/tests/09-fortigate/01-two-fortigates.robot new file mode 100644 index 000000000..bf5eb56ac --- /dev/null +++ b/tests/09-fortigate/01-two-fortigates.robot @@ -0,0 +1,50 @@ +*** Settings *** +Library OperatingSystem +Resource ../common.robot + +Suite Teardown Run Keyword Cleanup + + +*** Variables *** +${lab-name} forti +${lab-file-name} fortigates.clab.yml +${runtime} docker + + +*** Test Cases *** +Deploy ${lab-name} lab + Log ${CURDIR} + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${CLAB_BIN} --runtime ${runtime} deploy -t ${CURDIR}/${lab-file-name} + Log ${output} + Should Be Equal As Integers ${rc} 0 + IF ${rc} != 0 Fatal Error Failed to deploy ${lab-name} lab + +Wait for VM to reach running state + Sleep 30s + Wait Until Keyword Succeeds 120 5s VM is healthy + + +*** Keywords *** +VM is healthy + ${rc} ${output} = Run And Return Rc And Output + ... sudo -E ${runtime} exec clab-${lab-name}-forti1 cat /health + Log ${output} + + Should Be Equal As Integers ${rc} 0 + Should Contain ${output} 0 running + + ${rc2} ${output2} = Run And Return Rc And Output + ... sudo -E ${runtime} exec clab-${lab-name}-forti2 cat /health + Log ${output2} + + Should Be Equal As Integers ${rc2} 0 + Should Contain ${output2} 0 running + +Cleanup + # dump logs from VM + Run sudo -E ${runtime} logs clab-${lab-name}-forti1 &> /tmp/${lab-name}-forti1.log + ${contents} = OperatingSystem.Get File /tmp/${lab-name}-forti1.log + Log ${contents} + + Run sudo -E ${CLAB_BIN} --runtime ${runtime} destroy -t ${CURDIR}/${lab-file-name} --cleanup diff --git a/tests/09-fortigate/fortigates.clab.yml b/tests/09-fortigate/fortigates.clab.yml new file mode 100644 index 000000000..f0581b619 --- /dev/null +++ b/tests/09-fortigate/fortigates.clab.yml @@ -0,0 +1,13 @@ +name: forti +topology: + nodes: + forti1: + kind: fortinet_fortigate + image: ghcr.io/srl-labs/fortigate:7.0.14 + # image: registry.srlinux.dev/pub/fortinet_fortigate:7.0.14 + forti2: + kind: fortinet_fortigate + image: ghcr.io/srl-labs/fortigate:7.0.14 + # image: registry.srlinux.dev/pub/fortinet_fortigate:7.0.14 + links: + - endpoints: ["forti1:eth1", "forti2:eth1"]