diff --git a/Makefile b/Makefile index aae53abb..441ed052 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ packer-service_Wordpress: packer-alma8 ${DIR_EXPORT}/service_Wordpress.qcow2 packer-service_example: packer-alma8 ${DIR_EXPORT}/service_example.qcow2 @${INFO} "Packer service_example done" -packer-service_VRouter: packer-alpine318 ${DIR_EXPORT}/service_VRouter.qcow2 +packer-service_VRouter: packer-alpine319 ${DIR_EXPORT}/service_VRouter.qcow2 @${INFO} "Packer service_VRouter done" packer-service_Harbor: packer-ubuntu2204 ${DIR_EXPORT}/service_Harbor.qcow2 diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/.gitignore b/appliances/VRouter/DHCP4v2/coredhcp-onelease/.gitignore new file mode 100644 index 00000000..f5cb967e --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/.gitignore @@ -0,0 +1,12 @@ +# Ignore the coredhcp-onelease binary +coredhcp-onelease + +# Ignore client binary +client/coredhcp_client +client/client + +# Ignore the leases file +*.sqlite3 + +# Ignore config files +onelease-config.yml \ No newline at end of file diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/LICENSE b/appliances/VRouter/DHCP4v2/coredhcp-onelease/LICENSE new file mode 100644 index 00000000..40724678 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 coredhcp, 2024 OpenNebula Systems + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/README.md b/appliances/VRouter/DHCP4v2/coredhcp-onelease/README.md new file mode 100644 index 00000000..899034f8 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/README.md @@ -0,0 +1,84 @@ +# coredhcp-onelease VRouter plugin for OpenNebula + +This go module contains a wrapper for [coredhcp](https://github.com/coredhcp/coredhcp), that instantiates a coredhcp server for each interface indicated in the configuation file, allowing specifying configurations for requests coming from different interfaces. Those services could include our custom `onelease` plugin, which implements the OpenNebula IP Address lease based on the client's MAC address last four bytes (by default, the MAC address should start with the `02:00` prefix). We can also exclude some IPs from the lease adding them to the corresponding parameter. + +# Execution + +In order to run the server, you should execute the following commands: +``` +go build . +sudo ./coredhcp-onelease +``` + +The coredhcp server will look for a configuration YAML file, for instance located in this same directory (config.yml). See the section below in order to see how to configure it. + +# Configuration + +In order to load the plugin on the server, you should have a `config.yml` configuration file in one of the following places: +``` +* ./onelease-config.yml +* /coredhcp/onelease-config.yml +* /root/.coredhcp/onelease-config.yml +* /etc/coredhcp/onelease-config.yml +``` + +or you can pass the file explicitly when running the server with the `-c` option: + +``` +sudo ./coredhcp-onelease -c myconfig.yml +``` + + +The config file content should contain the list of plugins and their arguments for each protocol version (DHCPv6 and DHCPv4), e.g. + +``` +eth0: + server4: + listen: + - "%eth0" + plugins: + - lease_time: 3600s + - server_id: 192.168.100.1 + - dns: 8.8.8.8 8.8.4.4 + - router: 192.168.100.1 + - netmask: 255.255.255.0 + - onelease: leases-eth0.sqlite3 192.168.100.20 192.168.100.30 3600s --excluded-ips 192.168.100.22,192.168.100.25 + --mac2ip --mac2ip-prefix 04:00 +eth1: + server4: + listen: + - "%eth1" + plugins: + - lease_time: 3600s + - server_id: 172.100.10.1 + - dns: 8.8.8.8 8.8.4.4 + - router: 172.100.10.1 + - netmask: 255.255.255.0 + - onelease: leases-eth0.sqlite3 172.100.10.2 172.100.10.100 3600s --excluded-ips 172.100.10.50,172.100.10.60 + --mac2ip +``` + +The plugin parameters are +``` +onelease: +``` +Where optional parameters are: +* `--excluded-ips`: A comma-separated list of IPs from the range to be excluded from the allocation. +* `--mac2ip`: Enables MAC2IP address translation, i.e. it will allocate the specified IP from the least 4 bytes of the provided client MAC address (the IP should be in the lease range). +* `--mac2ip-prefix`: The MAC address 2-byte prefix for using in the MAC2IP feature (all the requests with this client MAC address 2-byte prefix will allocate the specified IP in the last 4 bytes). Defaults to "02:00". + +[There](https://github.com/coredhcp/coredhcp/blob/master/cmds/coredhcp/config.yml.example) you have an example of each interface configuration in case you want to take it as reference, but as we are using a wrapper, +remember to nest the configuration on each interface tag. + +# Testing + +You can test the server features using the [client](./client/README.md) included in this module or any dhcp client tool like `dhclient` or `dhcping`. + +# Maintenance + +This module `main.go` file is a wrapper of the Coredhcp module, based on the source code generated by the [coredhcp-generator](https://github.com/coredhcp/coredhcp/tree/master/cmds/coredhcp-generator) and adapted for spawning multiple Coredhcp servers on each specified interface. In order to add or remove any plugin you should modify the `main.go` file from this module directly, as if substantially differs from the one generated by the `coredhcp-generator`. + +# Licensing + +The original work from the Coredhcp team and all the OpenNebula Systems modifications are licensed under the MIT License included in this directory. + diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/README.md b/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/README.md new file mode 100644 index 00000000..11ad912e --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/README.md @@ -0,0 +1,14 @@ +# DHCPv4 debug client + +This is a simple dhcpv4 client for use as a debugging tool with coredhcp + +***This is not a general-purpose DHCP client. This is only a testing/debugging tool for developing CoreDHCP*** + +# Execution + +The client allows to specify a mac address as argument in order to include it in its requests, e.g. + +``` +go build -o coredhcp_client +sudo ./coredhcp_client "02:00:aa:bb:cc:dd" +``` diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/main.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/main.go new file mode 100644 index 00000000..83e5594f --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/client/main.go @@ -0,0 +1,54 @@ +package main + +/* + * Sample DHCPv4 client to test on the local interface + */ + +import ( + "flag" + "net" + "time" + + "github.com/coredhcp/coredhcp/logger" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/insomniacslk/dhcp/dhcpv4/client4" +) + +var log = logger.GetLogger("main") + +func main() { + flag.Parse() + + var macString string + if len(flag.Args()) > 0 { + macString = flag.Arg(0) + } else { + macString = "00:11:22:33:44:55" + } + + c := client4.NewClient() + c.LocalAddr = &net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 68, + } + c.RemoteAddr = &net.UDPAddr{ + IP: net.ParseIP("127.0.0.1"), + Port: 67, + } + c.ReadTimeout = 10 * time.Second + log.Printf("%+v", c) + + mac, err := net.ParseMAC(macString) + if err != nil { + log.Fatal(err) + } + + conv, err := c.Exchange("lo", + dhcpv4.WithHwAddr(mac)) + for _, p := range conv { + log.Print(p.Summary()) + } + if err != nil { + log.Fatal(err) + } +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.mod b/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.mod new file mode 100644 index 00000000..8591c2b1 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.mod @@ -0,0 +1,49 @@ +module github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/coredhcp-onelease + +go 1.20 + +require ( + github.com/coredhcp/coredhcp v0.0.0-20240908184240-576af8676ffa + github.com/insomniacslk/dhcp v0.0.0-20240829085014-a3a4c1f04475 + github.com/mattn/go-sqlite3 v1.14.22 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.9.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/bits-and-blooms/bitset v1.14.2 // indirect + github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/google/gopacket v1.1.19 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/josharian/native v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/nxadm/tail v1.4.11 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.25.0 // indirect + golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect + golang.org/x/net v0.27.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect +) diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.sum b/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.sum new file mode 100644 index 00000000..bf809c18 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/go.sum @@ -0,0 +1,129 @@ +github.com/bits-and-blooms/bitset v1.14.2 h1:YXVoyPndbdvcEVcseEovVfp0qjJp7S+i5+xgp/Nfbdc= +github.com/bits-and-blooms/bitset v1.14.2/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg= +github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8= +github.com/coredhcp/coredhcp v0.0.0-20240908184240-576af8676ffa h1:AR+9ZcTcEpOYtGwsUmr/yAq+BVBWSDdpkiVifn8U31c= +github.com/coredhcp/coredhcp v0.0.0-20240908184240-576af8676ffa/go.mod h1:grzl9xPCKrAp5eiApPLkfCRpRegtXxyiQTCSsceAMzA= +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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +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/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.14.0 h1:2mOpI4JVVPBN+WQRa0WKH2eXR+Ey+uK4n7Zj0aYpIQA= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +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/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/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= +github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c h1:zqmyTlQyufRC65JnImJ6H1Sf7BDj8bG31EV919NVEQc= +github.com/spf13/pflag v1.0.6-0.20201009195203-85dd5c8bc61c/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= +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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o= +golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +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.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +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-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +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= diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/main.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/main.go new file mode 100755 index 00000000..576b39d3 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/main.go @@ -0,0 +1,195 @@ +package main + +import ( + "fmt" + "io" + "os" + "os/signal" + "sync" + + oneleaseconfig "github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/coredhcp-onelease/pkg/config" + + "github.com/coredhcp/coredhcp/logger" + "github.com/coredhcp/coredhcp/server" + + coredhcpconfig "github.com/coredhcp/coredhcp/config" + + pl_onelease "github.com/OpenNebula/one-apps/appliances/VRouterd/DHCP4v2/coredhcp-onelease/plugins/onelease" + + "github.com/coredhcp/coredhcp/plugins" + pl_autoconfigure "github.com/coredhcp/coredhcp/plugins/autoconfigure" + pl_dns "github.com/coredhcp/coredhcp/plugins/dns" + pl_file "github.com/coredhcp/coredhcp/plugins/file" + pl_ipv6only "github.com/coredhcp/coredhcp/plugins/ipv6only" + pl_leasetime "github.com/coredhcp/coredhcp/plugins/leasetime" + pl_mtu "github.com/coredhcp/coredhcp/plugins/mtu" + pl_nbp "github.com/coredhcp/coredhcp/plugins/nbp" + pl_netmask "github.com/coredhcp/coredhcp/plugins/netmask" + pl_prefix "github.com/coredhcp/coredhcp/plugins/prefix" + pl_router "github.com/coredhcp/coredhcp/plugins/router" + pl_searchdomains "github.com/coredhcp/coredhcp/plugins/searchdomains" + pl_serverid "github.com/coredhcp/coredhcp/plugins/serverid" + pl_sleep "github.com/coredhcp/coredhcp/plugins/sleep" + pl_staticroute "github.com/coredhcp/coredhcp/plugins/staticroute" + + "github.com/sirupsen/logrus" + flag "github.com/spf13/pflag" +) + +var ( + flagLogFile = flag.StringP("logfile", "l", "", "Name of the log file to append to. Default: stdout/stderr only") + flagLogNoStdout = flag.BoolP("nostdout", "N", false, "Disable logging to stdout/stderr") + flagLogLevel = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels())) + flagConfig = flag.StringP("conf", "c", "", "Use this configuration file instead of the default location") + flagPlugins = flag.BoolP("plugins", "P", false, "list plugins") +) + +var logLevels = map[string]func(*logrus.Logger){ + "none": func(l *logrus.Logger) { l.SetOutput(io.Discard) }, + "debug": func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) }, + "info": func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) }, + "warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) }, + "error": func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) }, + "fatal": func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) }, +} + +func getLogLevels() []string { + var levels []string + for k := range logLevels { + levels = append(levels, k) + } + return levels +} + +var desiredPlugins = []*plugins.Plugin{ + &pl_autoconfigure.Plugin, + &pl_dns.Plugin, + &pl_file.Plugin, + &pl_ipv6only.Plugin, + &pl_leasetime.Plugin, + &pl_mtu.Plugin, + &pl_nbp.Plugin, + &pl_netmask.Plugin, + &pl_onelease.Plugin, + &pl_prefix.Plugin, + //&pl_range.Plugin, + &pl_router.Plugin, + &pl_searchdomains.Plugin, + &pl_serverid.Plugin, + &pl_sleep.Plugin, + &pl_staticroute.Plugin, +} + +var log = logger.GetLogger("main") + +func main() { + flag.Parse() + + if *flagPlugins { + for _, p := range desiredPlugins { + fmt.Println(p.Name) + } + os.Exit(0) + } + + fn, ok := logLevels[*flagLogLevel] + if !ok { + log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *flagLogLevel, getLogLevels()) + } + fn(log.Logger) + log.Infof("Setting log level to '%s'", *flagLogLevel) + if *flagLogFile != "" { + log.Infof("Logging to file %s", *flagLogFile) + logger.WithFile(log, *flagLogFile) + } + if *flagLogNoStdout { + log.Infof("Disabling logging to stdout/stderr") + logger.WithNoStdOutErr(log) + } + // register plugins + for _, plugin := range desiredPlugins { + if err := plugins.RegisterPlugin(plugin); err != nil { + log.Fatalf("Failed to register plugin '%s': %v", plugin.Name, err) + } + } + + // create a configuration file per interface + tempDir, configFilesMap, err := createInterfaceConfigFiles(*flagConfig) + if err != nil { + log.Fatalf("Failed to load configuration: %v", err) + } + defer cleanup(tempDir) + + // channel to listen for termination signals + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt) + + // channel to collect errors + errChan := make(chan error, len(configFilesMap)) + + // references to each interface listener server, for closing them later + serverMap := make(map[string]*server.Servers) + + // start a server per interface declared in the config + var wg sync.WaitGroup + for iface, configFile := range configFilesMap { + wg.Add(1) + go func(iface string, configFile string) { + defer wg.Done() + cfg, err := coredhcpconfig.Load(configFile) + if err != nil { + errChan <- fmt.Errorf("failed to load configuration for interface %s: %v", iface, err) + return + } + log.Infof("Starting listener for interface %s...", iface) + //no problem with concurrency because each goroutine access its own index + serverMap[iface], err = server.Start(cfg) + if err != nil { + errChan <- fmt.Errorf("failed to start listener for interface %s: %v", iface, err) + return + } + if err := serverMap[iface].Wait(); err != nil { + errChan <- fmt.Errorf("listener for interface %s failed: %v", iface, err) + return + } + }(iface, configFile) + } + + select { + case <-stop: + log.Info("Received SIGINT, shutting down...") + case err = <-errChan: + log.Errorf("Received error: %v, shutting down all listeners...", err) + } + + for iface, srv := range serverMap { + log.Infof("Shutting down listener for interface %s...", iface) + if srv != nil { + srv.Close() + } + } + + wg.Wait() + close(errChan) + close(stop) + + log.Infof("All listeners shut down, exiting") + if err != nil { + os.Exit(1) + } +} + +func createInterfaceConfigFiles(path string) (string, map[string]string, error) { + log.Infof("Loading configuration from %s", path) + + ifaceConfigs, err := oneleaseconfig.LoadConfig(path) + if err != nil { + return "", nil, err + } + + return oneleaseconfig.CreateTempConfigFiles(ifaceConfigs) +} + +func cleanup(tempDir string) { + oneleaseconfig.CleanupTempConfigFiles(tempDir) +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/pkg/config/oneconfig.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/pkg/config/oneconfig.go new file mode 100644 index 00000000..4df4fa17 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/pkg/config/oneconfig.go @@ -0,0 +1,89 @@ +package config + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/coredhcp/coredhcp/logger" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +var log = logger.GetLogger("oneconfig") + +type ServerConfig struct { + Plugins []map[string]string `yaml:"plugins"` + Listen []string `yaml:"listen"` +} + +type InterfaceConfig struct { + Server4 ServerConfig `yaml:"server4"` + //TODO Server6 for Ipv6 dhcp service not supported yet +} + +func LoadConfig(path string) (map[string]InterfaceConfig, error) { + viper.SetConfigType("yaml") + if path != "" { + viper.SetConfigFile(path) + } else { + viper.SetConfigName("onelease-config") + viper.AddConfigPath(".") + viper.AddConfigPath("$XDG_CONFIG_HOME/coredhcp/") + viper.AddConfigPath("$HOME/.coredhcp/") + viper.AddConfigPath("/etc/coredhcp/") + } + + if err := viper.ReadInConfig(); err != nil { + return nil, fmt.Errorf("error reading config file, %s", err) + } + + var config map[string]InterfaceConfig + if err := viper.Unmarshal(&config); err != nil { + return nil, fmt.Errorf("unable to decode into struct, %v", err) + } + + return config, nil +} + +// create a temporary dir and write there the per-interfafce configuration files +// return the temporary directory path and a map with all the config files paths +func CreateTempConfigFiles(config map[string]InterfaceConfig) (string, map[string]string, error) { + tempDir := filepath.Join(os.TempDir(), "one-coredhcp-config") + if err := os.MkdirAll(tempDir, 0755); err != nil { + return "", nil, fmt.Errorf("unable to create temporary directory, %v", err) + } + + configFiles := make(map[string]string) + for interfaceName, interfaceConfig := range config { + filePath, err := writePerInterfaceConfig(tempDir, interfaceName, interfaceConfig) + if err != nil { + return "", nil, err + } + configFiles[interfaceName] = filePath + } + return tempDir, configFiles, nil +} + +func writePerInterfaceConfig(path string, interfaceName string, + config InterfaceConfig) (string, error) { + data, err := yaml.Marshal(&config) + if err != nil { + return "", fmt.Errorf("unable to marshal config for interface '%s', %v", interfaceName, err) + } + + file := filepath.Join(path, interfaceName+"-config.yaml") + if err := os.WriteFile(file, data, 0644); err != nil { + return "", fmt.Errorf("unable to write config for interface '%s' to file, %v", interfaceName, err) + } + + return file, nil +} + +func CleanupTempConfigFiles(tempDir string) { + if err := os.RemoveAll(tempDir); err != nil { + log.Printf("Failed to clean up temporary files in %s: %v", tempDir, err) + } else { + log.Printf("Successfully cleaned up temporary files") + } +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin.go new file mode 100644 index 00000000..33f52eaa --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin.go @@ -0,0 +1,304 @@ +// Copyright 2018-present the CoreDHCP Authors. All rights reserved +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range +// Adapted by OpenNebula Systems for the VRouter appliance +// Copyright 2024-present OpenNebula Systems + +package onelease + +import ( + "database/sql" + "encoding/binary" + "errors" + "fmt" + "net" + "regexp" + "strconv" + "strings" + "sync" + "time" + + "github.com/coredhcp/coredhcp/handler" + "github.com/coredhcp/coredhcp/logger" + "github.com/coredhcp/coredhcp/plugins" + "github.com/coredhcp/coredhcp/plugins/allocators" + "github.com/coredhcp/coredhcp/plugins/allocators/bitmap" + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/spf13/pflag" +) + +var log = logger.GetLogger("plugins/onelease") + +// Plugin wraps plugin registration information +var Plugin = plugins.Plugin{ + Name: "onelease", + Setup4: setupRange, +} + +// Record holds an IP lease record +type Record struct { + IP net.IP + expires int + hostname string +} + +// PluginState is the data held by an instance of the range plugin +type PluginState struct { + // Rough lock for the whole plugin, we'll get better performance once we use leasestorage + sync.Mutex + // Recordsv4 holds a MAC -> IP address and lease time mapping + Recordsv4 map[string]*Record + LeaseTime time.Duration + ExcludedIPs []net.IP + leasedb *sql.DB + allocator allocators.Allocator + enableMAC2IP bool + MACPrefix [2]byte + rangeStartIP net.IP + rangeEndIP net.IP +} + +// Handler4 handles DHCPv4 packets for the range plugin +func (p *PluginState) Handler4(req, resp *dhcpv4.DHCPv4) (*dhcpv4.DHCPv4, bool) { + p.Lock() + defer p.Unlock() + record, ok := p.Recordsv4[req.ClientHWAddr.String()] + hostname := req.HostName() + if !ok { + // Allocating new address since there isn't one allocated + log.Printf("MAC address %s is new, leasing new IPv4 address", req.ClientHWAddr.String()) + // Check if the MAC address should be mapped to a specific IP address + ipToAllocate := net.IPNet{} + macPrefixMatches := false + if p.enableMAC2IP { + macAddress := req.ClientHWAddr + ipFromMAC, ok, err := p.checkMACPrefix(macAddress) + if err != nil { + log.Errorf("MAC2IP lease failed for mac %v: %v", macAddress.String(), err) + return nil, true + } + if ok { + macPrefixMatches = true + //propose the least 4 bytes of the mac address for allocating an IP address + ipToAllocate = net.IPNet{IP: ipFromMAC} + log.Infof("MAC %s matches the prefix %02x:%02x, trying to allocate IP %s...", macAddress.String(), p.MACPrefix[0], p.MACPrefix[1], ipToAllocate.IP.String()) + } else { + log.Infof("MAC %s does not match the prefix %x, providing conventional lease...", macAddress.String(), p.MACPrefix) + } + } + // The allocator will try to allocate the given IP address, but if it is already allocated, it will return a different one (an available one) + ip, err := p.allocator.Allocate(ipToAllocate) + if err != nil { + log.Errorf("Could not allocate IP for MAC %s: %v", req.ClientHWAddr.String(), err) + return nil, true + } + + // if the MAC address is mapped to an IP address, check if the allocated IP address matches the requested one, if not, revert the allocation and return + if p.enableMAC2IP && macPrefixMatches && !ip.IP.Equal(ipToAllocate.IP) { + p.allocator.Free(ip) + log.Errorf("MAC2IP: Could not allocate IP %s for MAC \"%s\": IP already allocated", ipToAllocate.IP.String(), req.ClientHWAddr.String()) + return nil, true + } + + rec := Record{ + IP: ip.IP.To4(), + expires: int(time.Now().Add(p.LeaseTime).Unix()), + hostname: hostname, + } + err = p.saveIPAddress(req.ClientHWAddr, &rec) + if err != nil { + log.Errorf("SaveIPAddress for MAC %s failed: %v", req.ClientHWAddr.String(), err) + } + p.Recordsv4[req.ClientHWAddr.String()] = &rec + record = &rec + } else { + // Ensure we extend the existing lease at least past when the one we're giving expires + expiry := time.Unix(int64(record.expires), 0) + if expiry.Before(time.Now().Add(p.LeaseTime)) { + record.expires = int(time.Now().Add(p.LeaseTime).Round(time.Second).Unix()) + record.hostname = hostname + err := p.saveIPAddress(req.ClientHWAddr, record) + if err != nil { + log.Errorf("Could not persist lease for MAC %s: %v", req.ClientHWAddr.String(), err) + } + } + } + resp.YourIPAddr = record.IP + resp.Options.Update(dhcpv4.OptIPAddressLeaseTime(p.LeaseTime.Round(time.Second))) + log.Printf("Found IP address %s for MAC %s", record.IP, req.ClientHWAddr.String()) + return resp, true +} + +func (p *PluginState) checkIPInRange(ip net.IP) bool { + return binary.BigEndian.Uint32(ip.To4()) >= binary.BigEndian.Uint32(p.rangeStartIP.To4()) && + binary.BigEndian.Uint32(ip.To4()) <= binary.BigEndian.Uint32(p.rangeEndIP.To4()) +} + +func (p *PluginState) checkMACPrefix(mac net.HardwareAddr) (net.IP, bool, error) { + // verify that the MAC address is valid + if _, err := net.ParseMAC(mac.String()); err != nil { + return nil, false, fmt.Errorf("invalid MAC address: %v", mac) + } + + // verify that the two first bytes equal to macPrefix, if not, return + if mac[0] != p.MACPrefix[0] || mac[1] != p.MACPrefix[1] { + return nil, false, nil + } + + // retrieve the IP address from the MAC address + // the IP address is the last 4 bytes of the MAC address + ip := net.IPv4(mac[2], mac[3], mac[4], mac[5]) + + // check if the ip is in the excluded list + for _, excluded := range p.ExcludedIPs { + if ip.Equal(excluded) { + return nil, false, fmt.Errorf("excluded IP %v", ip) + } + } + + // check if the ip is in the lease range + if !p.checkIPInRange(ip) { + return nil, false, fmt.Errorf("IP %v is not in the range", ip) + } + + return ip, true, nil +} + +func setupRange(args ...string) (handler.Handler4, error) { + var ( + err error + p PluginState + ) + + if len(args) < 4 { + return nil, fmt.Errorf("invalid number of arguments, want at least: 4 (file name, start IP, end IP, lease time), got: %d", len(args)) + } + filename := args[0] + if filename == "" { + return nil, errors.New("file name cannot be empty") + } + + p.rangeStartIP, p.rangeEndIP, err = parseIPRange(args[1], args[2]) + if err != nil { + return nil, fmt.Errorf("invalid IP range: %v", err) + } + + p.allocator, err = bitmap.NewIPv4Allocator(p.rangeStartIP, p.rangeEndIP) + if err != nil { + return nil, fmt.Errorf("could not create an allocator: %w", err) + } + + p.LeaseTime, err = time.ParseDuration(args[3]) + if err != nil { + return nil, fmt.Errorf("invalid lease duration: %v", args[3]) + } + + optionalArgs := args[4:] + + var excludedIPs string + var macPrefix string + + pluginFlags := pflag.NewFlagSet("onelease", pflag.ExitOnError) + + pluginFlags.StringVar(&excludedIPs, "excluded-ips", "", "Comma-separated list of excluded IP addresses") + pluginFlags.BoolVar(&p.enableMAC2IP, "mac2ip", false, "Enables MAC to IP address mapping") + pluginFlags.StringVar(&macPrefix, "mac2ip-prefix", "02:00", "2-byte MAC prefix for MAC to IP address mapping") + + pluginFlags.Parse(optionalArgs) + + if p.enableMAC2IP && macPrefix != "" { + p.MACPrefix, err = parseMACPrefix(macPrefix) + if err != nil { + return nil, fmt.Errorf("invalid MAC prefix: %v", macPrefix) + } + } + + if excludedIPs != "" { + p.ExcludedIPs, err = parseExcludedIPs(excludedIPs) + if err != nil { + return nil, fmt.Errorf("invalid excluded IPs: %v", excludedIPs) + } + //check if excluded IPs are in the range and pre-allocate them + for _, excluded := range p.ExcludedIPs { + if !p.checkIPInRange(excluded) { + log.Warnf("excluded IP %v is not in the range, not preallocation needed.", excluded) + continue + } + if _, err := p.allocator.Allocate(net.IPNet{IP: excluded}); err != nil { + return nil, fmt.Errorf("could not pre-allocate excluded IP %v: %w", excluded, err) + } + } + } + + if err := p.registerBackingDB(filename); err != nil { + return nil, fmt.Errorf("could not setup lease storage: %w", err) + } + p.Recordsv4, err = loadRecords(p.leasedb) + if err != nil { + return nil, fmt.Errorf("could not load records from file: %v", err) + } + + log.Printf("Loaded %d DHCPv4 leases from %s", len(p.Recordsv4), filename) + + for _, v := range p.Recordsv4 { + ip, err := p.allocator.Allocate(net.IPNet{IP: v.IP}) + if err != nil { + return nil, fmt.Errorf("failed to re-allocate leased ip %v: %v", v.IP.String(), err) + } + if ip.IP.String() != v.IP.String() { + return nil, fmt.Errorf("allocator did not re-allocate requested leased ip %v: %v", v.IP.String(), ip.String()) + } + } + + return p.Handler4, nil +} + +func parseIPRange(startIP, endIP string) (net.IP, net.IP, error) { + ipRangeStart := net.ParseIP(startIP) + if ipRangeStart.To4() == nil { + return nil, nil, fmt.Errorf("invalid IPv4 address: %v", startIP) + } + ipRangeEnd := net.ParseIP(endIP) + if ipRangeEnd.To4() == nil { + return nil, nil, fmt.Errorf("invalid IPv4 address: %v", endIP) + } + if binary.BigEndian.Uint32(ipRangeStart.To4()) >= binary.BigEndian.Uint32(ipRangeEnd.To4()) { + return nil, nil, errors.New("start of IP range has to be lower than the end of an IP range") + } + return ipRangeStart, ipRangeEnd, nil +} + +func parseExcludedIPs(ipList string) ([]net.IP, error) { + excludedIPs := []net.IP{} + for _, ip := range strings.Split(ipList, ",") { + excluded := net.ParseIP(strings.TrimSpace(ip)) + if excluded.To4() == nil { + return nil, fmt.Errorf("invalid excluded IP address: %v", ip) + } + excludedIPs = append(excludedIPs, excluded) + } + return excludedIPs, nil +} + +func parseMACPrefix(prefix string) ([2]byte, error) { + regex := `^[0-9A-Fa-f]{2}:[0-9A-Fa-f]{2}$` + matched, err := regexp.MatchString(regex, prefix) + if err != nil { + return [2]byte{}, fmt.Errorf("error matching regex: %v", err) + } + if !matched { + return [2]byte{}, fmt.Errorf("invalid MAC prefix format: %s", prefix) + } + parts := strings.Split(prefix, ":") + macByte0, err := strconv.ParseUint(parts[0], 16, 8) + if err != nil { + return [2]byte{}, fmt.Errorf("invalid MAC prefix byte [0]: %v", err) + } + macByte1, err := strconv.ParseUint(parts[1], 16, 8) + if err != nil { + return [2]byte{}, fmt.Errorf("invalid MAC prefix byte [1]: %v", err) + } + return [2]byte{byte(macByte0), byte(macByte1)}, nil +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin_test.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin_test.go new file mode 100644 index 00000000..8c89bce8 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/plugin_test.go @@ -0,0 +1,524 @@ +// Copyright 2018-present the CoreDHCP Authors. All rights reserved +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range +// Adapted by OpenNebula Systems for the VRouter appliance +// Copyright 2024-present OpenNebula Systems + +package onelease + +import ( + "encoding/binary" + "net" + "os" + "testing" + "time" + + "github.com/insomniacslk/dhcp/dhcpv4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTestPluginState() *PluginState { + startIP := net.ParseIP("192.168.1.1") + endIP := net.ParseIP("192.168.1.254") + + p := &PluginState{ + Recordsv4: make(map[string]*Record), + LeaseTime: 1 * time.Hour, + rangeStartIP: startIP, + rangeEndIP: endIP, + allocator: nil, + } + return p +} +func TestParseIPRange(t *testing.T) { + testCases := []struct { + name string + startIP string + endIP string + expectError bool + }{ + { + name: "Valid IP Range", + startIP: "192.168.1.1", + endIP: "192.168.1.254", + expectError: false, + }, + { + name: "Invalid Start IP", + startIP: "invalid", + endIP: "192.168.1.254", + expectError: true, + }, + { + name: "Invalid End IP", + startIP: "192.168.1.1", + endIP: "invalid", + expectError: true, + }, + { + name: "Start IP Greater Than End IP", + startIP: "192.168.1.255", + endIP: "192.168.1.1", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + startIP, endIP, err := parseIPRange(tc.startIP, tc.endIP) + + if tc.expectError { + assert.Error(t, err) + assert.Nil(t, startIP) + assert.Nil(t, endIP) + } else { + assert.NoError(t, err) + assert.NotNil(t, startIP) + assert.NotNil(t, endIP) + } + }) + } +} +func TestParseExcludedIPs(t *testing.T) { + testCases := []struct { + name string + ipList string + expectError bool + expectedLen int + }{ + { + name: "Valid Single IP", + ipList: "192.168.1.1", + expectError: false, + expectedLen: 1, + }, + { + name: "Valid Multiple IPs", + ipList: "192.168.1.1, 192.168.1.2, 192.168.1.3", + expectError: false, + expectedLen: 3, + }, + { + name: "Invalid IP", + ipList: "192.168.1.1, invalid, 192.168.1.3", + expectError: true, + expectedLen: 0, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + excludedIPs, err := parseExcludedIPs(tc.ipList) + + if tc.expectError { + assert.Error(t, err) + assert.Nil(t, excludedIPs) + } else { + assert.NoError(t, err) + assert.Len(t, excludedIPs, tc.expectedLen) + } + }) + } +} +func TestParseMACPrefix(t *testing.T) { + testCases := []struct { + name string + prefix string + expectError bool + expectedBytes [2]byte + }{ + { + name: "Valid MAC Prefix", + prefix: "02:00", + expectError: false, + expectedBytes: [2]byte{0x02, 0x00}, + }, + { + name: "Invalid Format", + prefix: "02-00", + expectError: true, + }, + { + name: "Invalid First Hex Character", + prefix: "GG:00", + expectError: true, + }, + { + name: "Invalid Second Hex Character", + prefix: "02:LL", + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + prefix, err := parseMACPrefix(tc.prefix) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Len(t, prefix, 2) + assert.Equal(t, tc.expectedBytes[0], prefix[0]) + assert.Equal(t, tc.expectedBytes[1], prefix[1]) + } + }) + } +} + +func TestCheckMACPrefix(t *testing.T) { + p := setupTestPluginState() + p.enableMAC2IP = true + p.MACPrefix = [2]byte{0x02, 0x00} + p.ExcludedIPs = []net.IP{net.ParseIP("192.168.1.2")} + + testCases := []struct { + name string + mac net.HardwareAddr + expectValid bool + expectedIP net.IP + }{ + { + name: "Valid MAC with Matching Prefix", + mac: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x19}, + expectValid: true, + expectedIP: net.IPv4(0xc0, 0xa8, 0x01, 0x19), + }, + { + name: "MAC with Non-Matching Prefix", + mac: net.HardwareAddr{0x01, 0x01, 0xc0, 0xa8, 0x01, 0x19}, + expectValid: false, + }, + { + name: "Excluded IP", + mac: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x02}, + expectValid: false, + }, + { + name: "Invalid MAC", + mac: nil, + expectValid: false, + expectedIP: nil, + }, + { + name: "MAC is not in the range", + mac: net.HardwareAddr{0x02, 0x00, 0x01, 0x01, 0x01, 0x01}, + expectValid: false, + expectedIP: nil, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ip, valid, err := p.checkMACPrefix(tc.mac) + + if tc.expectValid { + assert.NoError(t, err) + assert.True(t, valid) + assert.Equal(t, tc.expectedIP, ip) + } else { + assert.False(t, valid) + } + }) + } +} +func TestCheckIPInRange(t *testing.T) { + p := setupTestPluginState() + + testCases := []struct { + name string + ip net.IP + inRange bool + }{ + { + name: "IP in range", + ip: net.ParseIP("192.168.1.100"), + inRange: true, + }, + { + name: "IP below range", + ip: net.ParseIP("192.168.1.0"), + inRange: false, + }, + { + name: "IP above range", + ip: net.ParseIP("192.168.1.255"), + inRange: false, + }, + { + name: "IP in different range", + ip: net.ParseIP("192.168.2.20"), + inRange: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result := p.checkIPInRange(tc.ip) + assert.Equal(t, tc.inRange, result) + }) + } +} +func TestSetupRange(t *testing.T) { + deleteDB("test_leases.db") + testCases := []struct { + name string + args []string + expectedLeaseTime time.Duration + reqMACAddr net.HardwareAddr + expectedIP net.IP + expectSetUpError bool + expectHandlerError bool + }{ + { + name: "Get IP (none allocated and no MAC2IP)", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + }, + expectedLeaseTime: 1 * time.Hour, + reqMACAddr: net.HardwareAddr{0xff, 0x00, 0xc0, 0xa8, 0x01, 0x37}, + expectedIP: net.ParseIP("192.168.1.1"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Get second IP (one allocated and no MAC2IP)", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + }, + expectedLeaseTime: 1 * time.Hour, + reqMACAddr: net.HardwareAddr{0xff, 0x01, 0xc0, 0xa8, 0x01, 0x38}, + expectedIP: net.ParseIP("192.168.1.2"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Get non consecutive IP (192.168.1.7) with MAC2IP", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "0s", // lease time + "--mac2ip", // enable MAC2IP + "--mac2ip-prefix", "02:00", // MAC prefix + }, + expectedLeaseTime: 0 * time.Second, + reqMACAddr: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x07}, + expectedIP: net.ParseIP("192.168.1.7"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Get fifth IP (Two allocated, third and fourth excluded, and no MAC2IP)", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "0s", // lease time + "--excluded-ips", "192.168.1.3,192.168.1.4", // excluded IPs + }, + expectedLeaseTime: 0 * time.Second, + reqMACAddr: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x27}, + expectedIP: net.ParseIP("192.168.1.5"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Try to get already allocated IP (192.168.1.5) with MAC2IP", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + "--excluded-ips", "192.168.1.3,192.168.1.4", // excluded IPs + "--mac2ip", // enable MAC2IP + }, + expectedLeaseTime: 1 * time.Hour, + reqMACAddr: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x05}, + expectSetUpError: false, + expectHandlerError: true, + }, + { + name: "Get IP (192.168.1.6) with MAC2IP and invalid MAC prefix", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + "--excluded-ips", "192.168.1.3,192.168.1.4", // excluded IPs + "--mac2ip", // enable MAC2IP + }, + expectedLeaseTime: 1 * time.Hour, + reqMACAddr: net.HardwareAddr{0xef, 0x12, 0xc0, 0xa8, 0x01, 0xff}, + expectedIP: net.ParseIP("192.168.1.6"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Try to get out of range IP (192.168.1.255) with MAC2IP", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + "--mac2ip", // enable MAC2IP + }, + expectedLeaseTime: 1 * time.Hour, + reqMACAddr: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0xff}, + expectSetUpError: false, + expectHandlerError: true, + }, + { + name: "Extend expired IP lease with no MAC2IP (192.168.1.5)", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "60s", // lease time + "--excluded-ips", "192.168.1.255", // try to exclude out of range IP + }, + expectedLeaseTime: 60 * time.Second, + reqMACAddr: net.HardwareAddr{0x02, 0x00, 0xc0, 0xa8, 0x01, 0x27}, + expectedIP: net.ParseIP("192.168.1.5"), + expectSetUpError: false, + expectHandlerError: false, + }, + { + name: "Try to get IP with all IPs leased and no MAC2IP", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "60s", // lease time + "--excluded-ips", "192.168.1.3,192.168.1.4", // excluded IPs + }, + expectedLeaseTime: 60 * time.Second, + reqMACAddr: net.HardwareAddr{0x53, 0x21, 0xc0, 0xa8, 0x01, 0x64}, + expectSetUpError: false, + expectHandlerError: true, + }, + { + name: "Invalid Number of Arguments", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Empty Filename", + args: []string{ + "", // empty filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Invalid IP Range", + args: []string{ + "test_leases.db", // filename + "invalid", // invalid start IP + "192.168.1.7", // end IP + "1h", // lease time + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Not compatible IP range with previously defined", + args: []string{ + "test_leases.db", // filename + "0.0.0.0", // invalid start IP + "0.0.0.1", // end IP + "1h", // lease time + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Invalid Lease Time", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "invalid", // invalid lease time + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Invalid MAC Prefix", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + "--mac2ip", // enable MAC2IP + "--mac2ip-prefix", "invalid", // invalid MAC prefix + }, + expectSetUpError: true, + expectHandlerError: false, + }, + { + name: "Invalid Excluded IPs", + args: []string{ + "test_leases.db", // filename + "192.168.1.1", // start IP + "192.168.1.7", // end IP + "1h", // lease time + "--excluded-ips", "invalid", // invalid excluded IPs + }, + expectSetUpError: true, + expectHandlerError: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + handler, err := setupRange(tc.args...) + if tc.expectSetUpError { + assert.Error(t, err, "Expected an error for test case: %s", tc.name) + assert.Nil(t, handler, "Handler should be nil when there's an error") + return + } + require.NoError(t, err, "Unexpected error for test case: %s", tc.name) + assert.NotNil(t, handler, "Handler should not be nil") + + req := &dhcpv4.DHCPv4{ + ClientHWAddr: tc.reqMACAddr, + } + resp := &dhcpv4.DHCPv4{ + Options: make(dhcpv4.Options), + } + handler(req, resp) + if tc.expectHandlerError { + assert.Nil(t, resp.YourIPAddr) + } else { + assert.Equal(t, tc.expectedIP.To4(), resp.YourIPAddr.To4()) + assert.Equal(t, tc.expectedLeaseTime.Seconds(), + float64(binary.BigEndian.Uint32(resp.Options.Get(dhcpv4.OptionIPAddressLeaseTime)))) + } + }) + } + deleteDB("test_leases.db") +} + +func deleteDB(filePath string) { + err := os.Remove(filePath) + if err != nil { + log.Println("Error deleting the file:", err) + } +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage.go new file mode 100644 index 00000000..835ecba3 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage.go @@ -0,0 +1,95 @@ +// Copyright 2018-present the CoreDHCP Authors. All rights reserved +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range +// Adapted by OpenNebula Systems for the VRouter appliance +// Copyright 2024-present OpenNebula Systems + +package onelease + +import ( + "database/sql" + "errors" + "fmt" + "net" + + _ "github.com/mattn/go-sqlite3" +) + +func loadDB(path string) (*sql.DB, error) { + db, err := sql.Open("sqlite3", fmt.Sprintf("file:%s", path)) + if err != nil { + return nil, fmt.Errorf("failed to open database (%T): %w", err, err) + } + if _, err := db.Exec("create table if not exists leases4 (mac string not null, ip string not null, expiry int, hostname string not null, primary key (mac, ip))"); err != nil { + return nil, fmt.Errorf("table creation failed: %w", err) + } + return db, nil +} + +// loadRecords loads the DHCPv6/v4 Records global map with records stored on +// the specified file. The records have to be one per line, a mac address and an +// IP address. +func loadRecords(db *sql.DB) (map[string]*Record, error) { + rows, err := db.Query("select mac, ip, expiry, hostname from leases4") + if err != nil { + return nil, fmt.Errorf("failed to query leases database: %w", err) + } + defer rows.Close() + var ( + mac, ip, hostname string + expiry int + records = make(map[string]*Record) + ) + for rows.Next() { + if err := rows.Scan(&mac, &ip, &expiry, &hostname); err != nil { + return nil, fmt.Errorf("failed to scan row: %w", err) + } + hwaddr, err := net.ParseMAC(mac) + if err != nil { + return nil, fmt.Errorf("malformed hardware address: %s", mac) + } + ipaddr := net.ParseIP(ip) + if ipaddr.To4() == nil { + return nil, fmt.Errorf("expected an IPv4 address, got: %v", ipaddr) + } + records[hwaddr.String()] = &Record{IP: ipaddr, expires: expiry, hostname: hostname} + } + if err := rows.Err(); err != nil { + return nil, fmt.Errorf("failed lease database row scanning: %w", err) + } + return records, nil +} + +// saveIPAddress writes out a lease to storage +func (p *PluginState) saveIPAddress(mac net.HardwareAddr, record *Record) error { + stmt, err := p.leasedb.Prepare(`insert or replace into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)`) + if err != nil { + return fmt.Errorf("statement preparation failed: %w", err) + } + defer stmt.Close() + if _, err := stmt.Exec( + mac.String(), + record.IP.String(), + record.expires, + record.hostname, + ); err != nil { + return fmt.Errorf("record insert/update failed: %w", err) + } + return nil +} + +// registerBackingDB installs a database connection string as the backing store for leases +func (p *PluginState) registerBackingDB(filename string) error { + if p.leasedb != nil { + return errors.New("cannot swap out a lease database while running") + } + // We never close this, but that's ok because plugins are never stopped/unregistered + newLeaseDB, err := loadDB(filename) + if err != nil { + return fmt.Errorf("failed to open lease database %s: %w", filename, err) + } + p.leasedb = newLeaseDB + return nil +} diff --git a/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage_test.go b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage_test.go new file mode 100644 index 00000000..52b745b7 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/coredhcp-onelease/plugins/onelease/storage_test.go @@ -0,0 +1,103 @@ +// Copyright 2018-present the CoreDHCP Authors. All rights reserved +// This source code is licensed under the MIT license found in the +// LICENSE file in the root directory of this source tree. + +// Original code: https://github.com/coredhcp/coredhcp/tree/576af8676ffaff9c85800fae235f614cb65410bd/plugins/range +// Adapted by OpenNebula Systems for the VRouter appliance +// Copyright 2024-present OpenNebula Systems + +package onelease + +import ( + "database/sql" + "fmt" + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func testDBSetup() (*sql.DB, error) { + db, err := loadDB(":memory:") + if err != nil { + return nil, err + } + for _, record := range records { + stmt, err := db.Prepare("insert into leases4(mac, ip, expiry, hostname) values (?, ?, ?, ?)") + if err != nil { + return nil, fmt.Errorf("failed to prepare insert statement: %w", err) + } + defer stmt.Close() + if _, err := stmt.Exec(record.mac, record.ip.IP.String(), record.ip.expires, record.ip.hostname); err != nil { + return nil, fmt.Errorf("failed to insert record into test db: %w", err) + } + } + return db, nil +} + +var expire = int(time.Date(2000, 01, 01, 00, 00, 00, 00, time.UTC).Unix()) +var records = []struct { + mac string + ip *Record +}{ + {"02:00:00:00:00:00", &Record{IP: net.IPv4(10, 0, 0, 0), expires: expire, hostname: "zero"}}, + {"02:00:00:00:00:01", &Record{IP: net.IPv4(10, 0, 0, 1), expires: expire, hostname: "one"}}, + {"02:00:00:00:00:02", &Record{IP: net.IPv4(10, 0, 0, 2), expires: expire, hostname: "two"}}, + {"02:00:00:00:00:03", &Record{IP: net.IPv4(10, 0, 0, 3), expires: expire, hostname: "three"}}, + {"02:00:00:00:00:04", &Record{IP: net.IPv4(10, 0, 0, 4), expires: expire, hostname: "four"}}, + {"02:00:00:00:00:05", &Record{IP: net.IPv4(10, 0, 0, 5), expires: expire, hostname: "five"}}, +} + +func TestLoadRecords(t *testing.T) { + db, err := testDBSetup() + if err != nil { + t.Fatalf("Failed to set up test DB: %v", err) + } + + parsedRec, err := loadRecords(db) + if err != nil { + t.Fatalf("Failed to load records from file: %v", err) + } + + mapRec := make(map[string]*Record) + for _, rec := range records { + var ( + ip, mac, hostname string + expiry int + ) + if err := db.QueryRow("select mac, ip, expiry, hostname from leases4 where mac = ?", rec.mac).Scan(&mac, &ip, &expiry, &hostname); err != nil { + t.Fatalf("record not found for mac=%s: %v", rec.mac, err) + } + mapRec[mac] = &Record{IP: net.ParseIP(ip), expires: expiry, hostname: hostname} + } + + assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB") +} + +func TestWriteRecords(t *testing.T) { + pl := PluginState{} + if err := pl.registerBackingDB(":memory:"); err != nil { + t.Fatalf("Could not setup file") + } + + mapRec := make(map[string]*Record) + for _, rec := range records { + hwaddr, err := net.ParseMAC(rec.mac) + if err != nil { + // bug in testdata + panic(err) + } + if err := pl.saveIPAddress(hwaddr, rec.ip); err != nil { + t.Errorf("Failed to save ip for %s: %v", hwaddr, err) + } + mapRec[hwaddr.String()] = &Record{IP: rec.ip.IP, expires: rec.ip.expires, hostname: rec.ip.hostname} + } + + parsedRec, err := loadRecords(pl.leasedb) + if err != nil { + t.Fatal(err) + } + + assert.Equal(t, mapRec, parsedRec, "Loaded records differ from what's in the DB") +} diff --git a/appliances/VRouter/DHCP4v2/main.rb b/appliances/VRouter/DHCP4v2/main.rb new file mode 100644 index 00000000..73602626 --- /dev/null +++ b/appliances/VRouter/DHCP4v2/main.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require_relative '../vrouter.rb' +require 'yaml' + +module Service + module DHCP4v2 + extend self + + DEPENDS_ON = %w[Service::Failover] + + ONEAPP_VNF_DHCP4_ENABLED = env :ONEAPP_VNF_DHCP4_ENABLED, 'NO' + + ONEAPP_VNF_DHCP4_AUTHORITATIVE = env :ONEAPP_VNF_DHCP4_AUTHORITATIVE, 'YES' + + ONEAPP_VNF_DHCP4_MAC2IP_ENABLED = env :ONEAPP_VNF_DHCP4_MAC2IP_ENABLED, 'YES' + ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX = env :ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX, '02:00' + + ONEAPP_VNF_DHCP4_LEASE_TIME = env :ONEAPP_VNF_DHCP4_LEASE_TIME, '3600' + + ONEAPP_VNF_DHCP4_GATEWAY = env :ONEAPP_VNF_DHCP4_GATEWAY, nil + ONEAPP_VNF_DHCP4_DNS = env :ONEAPP_VNF_DHCP4_DNS, nil + + ONEAPP_VNF_DHCP4_INTERFACES = env :ONEAPP_VNF_DHCP4_INTERFACES, '' # nil -> none, empty -> all + + SERVICE_DIR = '/etc/one-appliance/service.d/VRouter/DHCP4v2/coredhcp-onelease' + CONFIG_FILE_NAME = 'onelease-config.yml' + + def parse_env + @interfaces ||= parse_interfaces ONEAPP_VNF_DHCP4_INTERFACES + @mgmt ||= detect_mgmt_nics + + interfaces = @interfaces.keys - @mgmt + + # generates a map between interfaces and their IP addresses (without netmask) + @nics2addrs ||= nics_to_addrs(interfaces).to_h { |nic, addr| [nic, addr[0..0]] } # aliases are unsupported + + # generates a map between IP addresses and their subnets identifiers with mask + @addrs2snets ||= addrs_to_subnets(interfaces).to_h { |a, s| [a.split(%[/])[0], s] } + + # generates a map between subnets identifiers and their allocatable IPs ranges + @snets2ranges ||= subnets_to_ranges(@addrs2snets.values) + + # generates a map between interfaces and array of its vip addresses (without netmask) + @vips ||= detect_vips.to_h { |nic, vip_map| [nic, vip_map.values.map { |vip| vip.split(%[/])[0] }] } + + # generates a map between interfaces and the endpoint addresses, taking VIPs into account + @ave ||= [detect_addrs, detect_vips].then do |addrs, vips| + [addrs, vips, detect_endpoints(addrs, vips)] + end.map(&:values).flatten.each_with_object({}) do |h, acc| + hashmap.combine! acc, h + end + + interfaces.each_with_object({}) do |nic, vars| + (snet, rng) = env("ONEAPP_VNF_DHCP4_#{nic.upcase}", nil)&.split(%[:])&.map(&:strip) + + @nics2addrs[nic]&.each do |addr| + subnet = snet || @addrs2snets[addr] + range = rng || @snets2ranges[@addrs2snets[addr]] + + vars[nic] ||= [] + vars[nic] << { + address: addr, + subnet: subnet, + range: range, + + gateway: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_GATEWAY", ONEAPP_VNF_DHCP4_GATEWAY).then do |gw| + # interpolates in case gw address comes in the form of a nic reference, e.g. + backends.interpolate(gw, @ave) unless gw.nil? + end, + + dns: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_DNS", ONEAPP_VNF_DHCP4_DNS).then do |dns| + # interpolates in case dns address comes in the form of a nic reference, e.g. + backends.interpolate(dns, @ave) unless dns.nil? + end, + + mtu: env("ONEAPP_VNF_DHCP4_#{nic.upcase}_MTU", ip_link_show(nic)['mtu']), + + vips: @vips[nic].to_a.select do |vip| + IPAddr.new(subnet).include?(vip) # exclude VIPs from outside of the subnet + end + } + end + end + end + + def generate_config(basedir, vars) + config = vars.each_with_object({}) do |(nic, vars), acc| + nic_data = vars[0] # For now, we don't support nic aliases + acc[nic] = { + 'server4' => { + 'listen' => ["%#{nic}"], + 'plugins' => [].then do |acc| + acc << {'lease_time' => "#{ONEAPP_VNF_DHCP4_LEASE_TIME}s"} + acc << {'server_id' => nic_data[:address]} + acc << {'dns' => nic_data[:dns]} unless nic_data[:dns].nil? + acc << {'mtu' => nic_data[:mtu]} unless nic_data[:mtu].nil? + acc << {'router' => nic_data[:gateway]} unless nic_data[:gateway].nil? + acc << {'netmask' => IPAddr.new('255.255.255.255').mask(nic_data[:subnet].split(%[/])[1]).to_s} + acc << {'onelease' => generate_onelease_config(nic, nic_data)} + acc + end + }, + } + end + + file "#{basedir}/#{CONFIG_FILE_NAME}", config.to_yaml, mode: 'u=rw,g=r,o=', overwrite: true + end + + def generate_onelease_config(nic, nic_data) + lease_range = nic_data[:range]&.gsub('-', ' ') + excluded_ips_str = ([nic_data[:address]] + nic_data[:vips]).join(',') + onelease_config = "leases-#{nic}.sqlite3 #{lease_range} #{ONEAPP_VNF_DHCP4_LEASE_TIME}s" + onelease_config += " --excluded-ips #{excluded_ips_str}" unless excluded_ips_str.empty? + onelease_config += " --mac2ip" if ONEAPP_VNF_DHCP4_MAC2IP_ENABLED + onelease_config += " --mac2ip-prefix #{ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX}" if ONEAPP_VNF_DHCP4_MAC2IP_ENABLED + return onelease_config + end + + def install(initdir: '/etc/init.d') + msg :info, 'DHCP4v2::install' + + file "#{initdir}/one-dhcp4v2", <<~SERVICE, :mode => 'u=rwx,g=rx,o=' + #!/sbin/openrc-run + source /run/one-context/one_env + + BASE_DIR="#{SERVICE_DIR}" + CONFIG_FILE="$BASE_DIR/#{CONFIG_FILE_NAME}" + SERVICE_EXEC="$BASE_DIR/coredhcp-onelease" + PIDFILE="/run/$RC_SVCNAME.pid" + LOG_DIR="#{SERVICE_LOGDIR}" + LOG_FILE="$LOG_DIR/$RC_SVCNAME.log" + + command="$SERVICE_EXEC" + command_args="-c $CONFIG_FILE" + command_background="yes" + pidfile="$PIDFILE" + + output_log="$LOG_FILE" + error_log="$LOG_FILE" + + depend() { + after net firewall keepalived + } + SERVICE + toggle [:update] + end + + def configure(basedir: SERVICE_DIR) + msg :info, 'DHCP4v2::configure' + + unless ONEAPP_VNF_DHCP4_ENABLED + # NOTE: We always disable it at re-contexting / reboot in case an user enables it manually. + toggle [:stop, :disable] + return + end + + dhcp4_vars = parse_env + + generate_config(basedir, dhcp4_vars) + + end + + def toggle(operations) + operations.each do |op| + msg :debug, "DHCP4v2::toggle([:#{op}])" + case op + when :disable + puts bash 'rc-update del one-dhcp4v2 default ||:' + when :update + puts bash 'rc-update -u' + else + puts bash "rc-service one-dhcp4v2 #{op.to_s}" + end + end + end + + def bootstrap + msg :info, 'DHCP4v2::bootstrap' + end + end +end diff --git a/appliances/VRouter/DHCP4v2/tests.rb b/appliances/VRouter/DHCP4v2/tests.rb new file mode 100644 index 00000000..bdc97bfd --- /dev/null +++ b/appliances/VRouter/DHCP4v2/tests.rb @@ -0,0 +1,198 @@ +# frozen_string_literal: true + +require 'rspec' +require 'tmpdir' + +def clear_env + ENV.delete_if { |name| name.start_with?('ETH') || name.include?('VROUTER_') || name.include?('_VNF_') } +end + +def clear_vars(object) + object.instance_variables.each { |name| object.remove_instance_variable(name) } +end + +RSpec.describe self do + it 'should provide and parse all env vars' do + clear_env + + ENV['ONEAPP_VNF_DHCP4_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DHCP4_AUTHORITATIVE'] = 'YES' + + ENV['ONEAPP_VNF_DHCP4_MAC2IP_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DHCP4_MAC2IP_MACPREFIX'] = '02:00' + + ENV['ONEAPP_VNF_DHCP4_LEASE_TIME'] = '3600' + + ENV['ONEAPP_VNF_DHCP4_GATEWAY'] = '1.2.3.4' + ENV['ONEAPP_VNF_DHCP4_DNS'] = '1.1.1.1' + + ENV['ONEAPP_VNF_DHCP4_INTERFACES'] = 'eth0 eth1 eth2 eth3' + ENV['ETH0_VROUTER_MANAGEMENT'] = 'YES' + + ENV['ONEAPP_VNF_DHCP4_ETH2'] = '30.0.0.0/8:30.40.50.64-30.40.50.68' + ENV['ONEAPP_VNF_DHCP4_ETH2_GATEWAY'] = '30.40.50.1' + ENV['ONEAPP_VNF_DHCP4_ETH2_DNS'] = '8.8.8.8' + + ENV['ONEAPP_VNF_DHCP4_ETH3_GATEWAY'] = '40.50.60.1' + ENV['ONEAPP_VNF_DHCP4_ETH3_DNS'] = '8.8.4.4' + + ENV['ETH0_IP'] = '10.20.30.40' + ENV['ETH0_MASK'] = '255.255.255.0' + + ENV['ETH1_IP'] = '20.30.40.50' + ENV['ETH1_MASK'] = '255.255.0.0' + + ENV['ETH1_ALIAS0_IP'] = '5.6.7.8' # ignored (unsupported) + ENV['ETH1_ALIAS0_MASK'] = '255.255.255.0' # ignored (unsupported) + + ENV['ETH2_IP'] = '30.40.50.60' + ENV['ETH2_MASK'] = '255.0.0.0' + + ENV['ETH3_IP'] = '40.50.60.70' + ENV['ETH3_MASK'] = '255.255.255.0' + + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '20.30.40.55' + ENV['ONEAPP_VROUTER_ETH1_VIP1'] = '1.2.3.4' # ignored (not in the subnet) + + load './main.rb'; include Service::DHCP4v2 + + allow(Service::DHCP4v2).to receive(:ip_link_show).and_return( + { 'mtu' => 1111 }, + { 'mtu' => 2222 }, + { 'mtu' => 3333 } + ) + + clear_vars Service::DHCP4v2 + + expect(Service::DHCP4v2.parse_env).to eq ({ + 'eth1' => [ { address: '20.30.40.50', + dns: '1.1.1.1', + gateway: '1.2.3.4', + mtu: 1111, + range: '20.30.0.2-20.30.255.254', + subnet: '20.30.0.0/16', + vips: %w[20.30.40.55] } ], + + 'eth2' => [ { address: '30.40.50.60', + dns: '8.8.8.8', + gateway: '30.40.50.1', + mtu: 2222, + range: '30.40.50.64-30.40.50.68', + subnet: '30.0.0.0/8', + vips: %w[] } ], + + 'eth3' => [ { address: '40.50.60.70', + dns: '8.8.4.4', + gateway: '40.50.60.1', + mtu: 3333, + range: '40.50.60.2-40.50.60.254', + subnet: '40.50.60.0/24', + vips: %w[] } ] + }) + + output = <<~'ONELEASE_CONF' + --- + eth1: + server4: + listen: + - "%eth1" + plugins: + - lease_time: 3600s + - server_id: 20.30.40.50 + - dns: 1.1.1.1 + - mtu: 1111 + - router: 1.2.3.4 + - netmask: 255.255.0.0 + - onelease: leases-eth1.sqlite3 20.30.0.2 20.30.255.254 3600s --excluded-ips 20.30.40.50,20.30.40.55 + --mac2ip --mac2ip-prefix 02:00 + eth2: + server4: + listen: + - "%eth2" + plugins: + - lease_time: 3600s + - server_id: 30.40.50.60 + - dns: 8.8.8.8 + - mtu: 2222 + - router: 30.40.50.1 + - netmask: 255.0.0.0 + - onelease: leases-eth2.sqlite3 30.40.50.64 30.40.50.68 3600s --excluded-ips 30.40.50.60 + --mac2ip --mac2ip-prefix 02:00 + eth3: + server4: + listen: + - "%eth3" + plugins: + - lease_time: 3600s + - server_id: 40.50.60.70 + - dns: 8.8.4.4 + - mtu: 3333 + - router: 40.50.60.1 + - netmask: 255.255.255.0 + - onelease: leases-eth3.sqlite3 40.50.60.2 40.50.60.254 3600s --excluded-ips 40.50.60.70 + --mac2ip --mac2ip-prefix 02:00 + ONELEASE_CONF + + allow(Service::DHCP4v2).to receive(:ip_link_show).and_return( + { 'mtu' => 1111 }, + { 'mtu' => 2222 }, + { 'mtu' => 3333 } + ) + + Dir.mktmpdir do |dir| + Service::DHCP4v2.configure basedir: dir + result = File.read "#{dir}/onelease-config.yml" + expect(result.strip).to eq output.strip + end + end + + it 'should interpolate GW and DNS values' do + clear_env + + ENV['ONEAPP_VNF_DHCP4_ENABLED'] = 'YES' + ENV['ONEAPP_VNF_DHCP4_INTERFACES'] = 'eth0 eth1' + + ENV['ONEAPP_VNF_DHCP4_ETH0_GATEWAY'] = '' + ENV['ONEAPP_VNF_DHCP4_ETH0_DNS'] = '' + + ENV['ONEAPP_VNF_DHCP4_ETH1_GATEWAY'] = '' + ENV['ONEAPP_VNF_DHCP4_ETH1_DNS'] = '' + + ENV['ETH0_IP'] = '10.20.30.40' + ENV['ETH0_MASK'] = '255.255.255.0' + + ENV['ETH1_IP'] = '20.30.40.50' + ENV['ETH1_MASK'] = '255.255.255.0' + + ENV['ONEAPP_VROUTER_ETH0_VIP0'] = '10.20.30.45' + ENV['ONEAPP_VROUTER_ETH1_VIP0'] = '20.30.40.55' + ENV['ONEAPP_VROUTER_ETH1_VIP1'] = '20.30.40.110' + + load './main.rb'; include Service::DHCP4v2 + + allow(Service::DHCP4v2).to receive(:ip_link_show).and_return( + { 'mtu' => 1111 }, + { 'mtu' => 2222 } + ) + + clear_vars Service::DHCP4v2 + + expect(Service::DHCP4v2.parse_env).to eq ({ + 'eth0' => [ { address: '10.20.30.40', + dns: '10.20.30.45', + gateway: '10.20.30.45', + mtu: 1111, + range: '10.20.30.2-10.20.30.254', + subnet: '10.20.30.0/24', + vips: %w[10.20.30.45] } ], + + 'eth1' => [ { address: '20.30.40.50', + dns: '20.30.40.110', + gateway: '20.30.40.55', + mtu: 2222, + range: '20.30.40.2-20.30.40.254', + subnet: '20.30.40.0/24', + vips: %w[20.30.40.55 20.30.40.110] } ] + }) + end +end diff --git a/appliances/VRouter/Failover/execute.rb b/appliances/VRouter/Failover/execute.rb index 474792b8..8b0c0b21 100644 --- a/appliances/VRouter/Failover/execute.rb +++ b/appliances/VRouter/Failover/execute.rb @@ -27,7 +27,7 @@ module Failover 'one-dns' => { _ENABLED: 'ONEAPP_VNF_DNS_ENABLED', fallback: 'NO' }, - 'one-dhcp4' => { _ENABLED: 'ONEAPP_VNF_DHCP4_ENABLED', + 'one-dhcp4v2' => { _ENABLED: 'ONEAPP_VNF_DHCP4_ENABLED', fallback: 'NO' }, 'one-wg' => { _ENABLED: 'ONEAPP_VNF_WG_ENABLED', diff --git a/packer/service_VRouter/10-update.sh b/packer/service_VRouter/10-update.sh index c72e81e6..61faffd2 100644 --- a/packer/service_VRouter/10-update.sh +++ b/packer/service_VRouter/10-update.sh @@ -9,6 +9,6 @@ service haveged stop ||: apk update -apk add bash curl ethtool gawk grep iproute2 jq ruby sed tcpdump +apk add bash curl ethtool gawk grep iproute2 jq ruby sed tcpdump go iptables sync diff --git a/packer/service_VRouter/VRouter.pkr.hcl b/packer/service_VRouter/VRouter.pkr.hcl index 1e8d488c..010b8979 100644 --- a/packer/service_VRouter/VRouter.pkr.hcl +++ b/packer/service_VRouter/VRouter.pkr.hcl @@ -18,7 +18,7 @@ source "qemu" "VRouter" { memory = 2048 accelerator = "kvm" - iso_url = "export/alpine318.qcow2" + iso_url = "export/alpine319.qcow2" iso_checksum = "none" headless = var.headless @@ -87,11 +87,25 @@ build { sources = ["appliances/VRouter"] destination = "/etc/one-appliance/service.d/" } + # Exclude DHCP4 legacy version + provisioner "shell" { + inline_shebang = "/bin/bash -e" + inline = ["rm -rf /etc/one-appliance/service.d/VRouter/DHCP4"] + } provisioner "shell" { scripts = ["${var.input_dir}/82-configure-context.sh"] } - + provisioner "shell" { + inline_shebang = "/bin/bash -e" + environment_vars = [ + "COREDHCP_ONELEASE_DIR=/etc/one-appliance/service.d/VRouter/DHCP4v2/coredhcp-onelease", + ] + inline = [ + "CGO_ENABLED=1 GCC=musl-gcc go build -C $COREDHCP_ONELEASE_DIR", + "find $COREDHCP_ONELEASE_DIR \\( -type f ! -name 'coredhcp-onelease' -o -type d -empty \\) -delete", + ] + } provisioner "shell" { inline_shebang = "/bin/bash -e" inline = ["/etc/one-appliance/service install && sync"]