diff --git a/.github/actions/rpm/action.yml b/.github/actions/rpm/action.yml index 9f3ce0c2..6132bfdc 100644 --- a/.github/actions/rpm/action.yml +++ b/.github/actions/rpm/action.yml @@ -67,8 +67,14 @@ runs: - name: Build RPMs and run tests run: | - mock -r ${{ inputs.target }} --rebuild --spec=warewulf.spec --sources=. - mock -r ${{ inputs.target }} --chroot -- make -C /builddir/build/BUILD/warewulf-${{ env.EXPECTED_VERSION }} test + root="${{ inputs.target }}" + eol_root="/etc/mock/eol/${root}.cfg" + if [ -f "${eol_root}" ] + then + root="${eol_root}" + fi + mock --root="${root}" --rebuild --spec=warewulf.spec --sources=. + mock --root="${root}" --chroot -- make -C /builddir/build/BUILD/warewulf-${{ env.EXPECTED_VERSION }} test shell: bash - name: Upload RPM diff --git a/.github/releases/v4.0.0.md b/.github/releases/v4.0.0.md new file mode 100644 index 00000000..055067d6 --- /dev/null +++ b/.github/releases/v4.0.0.md @@ -0,0 +1 @@ +Initial release to test build and packaging pipeline diff --git a/.github/releases/v4.1.0.md b/.github/releases/v4.1.0.md new file mode 100644 index 00000000..08fb994d --- /dev/null +++ b/.github/releases/v4.1.0.md @@ -0,0 +1,38 @@ +### Added +- Support for ARM nodes +- firewalld service file for warewulfd +- `-y` option to skip "Are you sure" queries +- `wwctl kernel delete` command +- `wwctl vnfs` alias for `wwctl container` +- Support for authenticated OCI registries +- warewulfd can reload config on SIGHUP and when the config file changes +- Node database index to improve lookup speeds +- Kernels and containers can be imported from a chroot subdirectory +- Systemd service file + +### Changed +- `wwctl node list` output beautification +- Log timestamps are more precise +- PID file and log files are now in `/var/run` and `/var/log`, respectively +- `make install` no longer overwrites preexisting configuration files +- Kernel modules and overlays are now compressed +- `rootfstype` now uses `rootfs` in default kernel arguments +- iPXE binaries updated +- Installed container directory is deleted when import fails +- Default iPXE script now reboots erroring nodes every 15 seconds +- Only open `/etc/hosts` when writing + +### Removed +- `wwctl configure` `--persist` flags have been removed. `configure` commands persist changes by default unless `--show` is used +- In-repository documentation: has been moved to it's own repository + +### Fixed +- Importing containers from directory +- Debug log verbosity option takes precedence over verbose option +- `wwctl node list -n` output is formatted corectly +- Container names can contain an underscore +- `wwctl overlay build --all` does not require an argument +- specfile date format works with older versions of rpmbuild +- Use SystemOverlay when building system overlay +- dhcpd template now references correct wwctl subcommand +- `wwctl node set kernelargs` and `wwctl profile set kernelargs` change kernel arguments diff --git a/.github/releases/v4.2.0.md b/.github/releases/v4.2.0.md new file mode 100644 index 00000000..e5417610 --- /dev/null +++ b/.github/releases/v4.2.0.md @@ -0,0 +1,5 @@ +We are pleased to announce the official release of Warewulf version 4.2.0! + +Please see the attached release tarball. + +Update 2021-12-22: RPMs have been rebuilt due to CVE-2021-44717 diff --git a/.github/releases/v4.2.0rc1.md b/.github/releases/v4.2.0rc1.md new file mode 100644 index 00000000..a368676f --- /dev/null +++ b/.github/releases/v4.2.0rc1.md @@ -0,0 +1 @@ +This is a pre-release of Warewulf 4.2.0. Please test! diff --git a/.github/releases/v4.3.0.md b/.github/releases/v4.3.0.md new file mode 100644 index 00000000..c22805a1 --- /dev/null +++ b/.github/releases/v4.3.0.md @@ -0,0 +1,22 @@ +The Warewulf development team is happy to release Warewulf version 4.3.0! + +# Changes +- The kernel is now part of the container by default which means the entire kernel and user space stack can be built, maintained, and distributed within a single container. It is still possible to override a specific kernel using the `--kerneloverride` option to nodes and profiles. +- Network interfaces are no longer named as the "device name", but rather have a free from string as the network name with the initial name being "default". +- Creating of '/etc/exports' can now be disabled, so that `wwctl configure -a` wont overwrite a existing '/etc/exports'. +- All configurations files for the host (/etc/exports, /etc/dhcpd.conf, /etc/hosts) are now populated from the `host` overlay. Also other configuration files like `prometheus.yml.ww` or `slurm.conf.ww` which depend on the cluster nodes can be placed there. +- New template functions now exist: `{{ abort }}`, `{{ IncludeBlock }}` and `{{ no_backup }}` are now supported. +- Nodes can now have multiple system and runtime overlays, as a comma separated list. +- Simple ipv6 support is now enabled. In `warewulf.conf` the option `ipaddr6`/`Ipv6net` must be set to enable ipv6. +- Multiple files can now be created from a single template overlay file with the `{{ file FILENAME }}` command in the template. The command is expanded to the magic template command `{{ /* file FILENAME */}}` which is picked up by wwctl and everything which comes after this magic comment will be written to the file `FILENAME`. This mechanism is leveraged in the configuration files for the network, see `ifcfg.xml.ww` and `ifcgf.ww`. +- Networks can now have arbitrary keys value pairs in the profiles and on the node, so that things like bridges and mtu sizes can be set +- The yaml format for nodes has now sub-keys for ipmi and kernel, old nodes.conf files have to be changed accordingly +- uids and gids of a container now get synced at import time, so that at least users with the same name have the same uid. This is not necessarily needed for warewulf, but services like munge. +- Refactoring of the Warewulf daemon to support better concurrency and building overlays on the fly. + +If you have any problems please join our Slack channel and/or submit an issue here: https://github.com/hpcng/warewulf/issues + +This is also release has also breaking changes for the configuration files, as the format for the network devices in the `nodes.conf` has changed, Also `warewulf.conf` has a new way to configure the nfs exports on the master node and separate sections for the ipmi and kernel configs. + +_note: From this release on, the main branch will only get bug fixes until the next release. Development will happen in the develop branch._ + diff --git a/.github/releases/v4.3.0rc1.md b/.github/releases/v4.3.0rc1.md new file mode 100644 index 00000000..2f90c49a --- /dev/null +++ b/.github/releases/v4.3.0rc1.md @@ -0,0 +1,33 @@ +First release candidate of the 4.3.0 branch. From this release on, the main branch will only get bug fixes until the next release. Development will happen in the develop branch. +This is also release has also breaking changes for the configuration files, as the format for the network devices in the `nodes.conf` has changed, Also `warewulf.conf` has a new way to configure the nfs exports on the master node and separate sections for the ipmi and kernel configs. + +# Changes +- Provision interface is not tied to 'eth0' any more. The provision interface must be named + 'default' now. The file `nodes.yaml' must be changed accordingly. +- Creating of '/etc/exports' can now be disabled, so that `wwctl configure -a` wont overwrite + a existing '/etc/exports'. +- All configurations files for the host (/etc/exports, /etc/dhcpd.conf, /etc/hosts) are now + populated from the (OVERLAYDIR/host/etc/{exports|dhcpd|hosts}.ww . Also other configuration + files like prometheus.yml.ww or slurm.conf.ww which depend on the cluster nodes can be + placed. Also the new templated functions {{ abort }}, {{ IncludeBlock }} abd {{ no_backup }} + are allowed now. +- nodes and profiles can now have multiple system and runtime overlays, as a comma separated list. + The overlays of the profile and the nodes are combined. +- simple ipv6 support is now enabled. In `warewulf.conf` the option `ipaddr6`/`Ipv6net` must + be set to enable ipv6. If enabled on of these options is set a node will get a derived + ipv6 in the scheme `ipv6net:ipaddr4`. This address can also be overwritten for every + node +- Multiple files can now created from a single `template.ww` file with the `{{ file FILENAME }}` + command in the template. The command is expanded to the magic template command + `{{ /* file FILENAME */}}` which is picked up by wwctl and everything which comes after this + magic comment will be written to the file `FILENAME`. This mechanism is leveraged in the + configuration files for the network, see `ifcfg.xml.ww` and `ifcgf.ww`. +- Networks can now have arbitrary keys value pairs in the profiles and on the node, so that + things like bridges and mtu sizes can be set +- The yaml format for nodes has now sub-keys for ipmi and kernel, old nodes.conf files have to + to be changed accordingly +- uids and gids of a container now get synced at import time, so that at least users with the + same name have the same uid. This is not necessarily needed for warewulf, but services like + munge. + + diff --git a/.github/releases/v4.3.0rc2.md b/.github/releases/v4.3.0rc2.md new file mode 100644 index 00000000..de9eecf2 --- /dev/null +++ b/.github/releases/v4.3.0rc2.md @@ -0,0 +1,3 @@ +* Includes bug fixes for `wwctl node|profile list` +* backward compatibility for `nodes.conf` +* added `file://` uri for importing archived containers diff --git a/.github/releases/v4.3.0rc3.md b/.github/releases/v4.3.0rc3.md new file mode 100644 index 00000000..5243b1d7 --- /dev/null +++ b/.github/releases/v4.3.0rc3.md @@ -0,0 +1,2 @@ +* host overlays are now enabled in standard config +* provision network is now called 'primary` and not 'default' diff --git a/.github/releases/v4.3.0rc4.md b/.github/releases/v4.3.0rc4.md new file mode 100644 index 00000000..f6c34573 --- /dev/null +++ b/.github/releases/v4.3.0rc4.md @@ -0,0 +1,25 @@ +- Updated container recipes +- Updated overlay templates +- Cleaned iPXE output + +Fixes for: #346 + +**Refactors** +* Combine server response handling, reduce code duplication +* Simplify stage cases, backward compatible uri +* Implement image creation as functions instead of shell scripts +* Combine daemonLogf and wwlog to single logging output +* Switch to named wwlog functions instead of constants + +**Fixes** +* Fix hidden errors occurring in shell pipes +* Fix kernel search path for Ubuntu +* Fix http response when template rendering fails +* Fix template newline insertion, update dhcp and hosts tmpl + +**Updates** +* Add hwaddr and interface to dhcp +* Add control over image compression to client +* Update IPXE template to handle non-compressed files +* Update DHCP template to set network interfaces and server IP assignment +* Update DHCP/hosts templates to choose a host-name self-consistently diff --git a/.github/releases/v4.3.0rc5.md b/.github/releases/v4.3.0rc5.md new file mode 100644 index 00000000..a20e2c29 --- /dev/null +++ b/.github/releases/v4.3.0rc5.md @@ -0,0 +1 @@ +Minor fixes that I missed in RC4. diff --git a/.github/releases/v4.3.0rc6.md b/.github/releases/v4.3.0rc6.md new file mode 100644 index 00000000..678c8c42 --- /dev/null +++ b/.github/releases/v4.3.0rc6.md @@ -0,0 +1,3 @@ +This is hopefully the last release candidate before the 4.3.0 release which is scheduled for next week (June 15th, 2022). + +In this RC is a substantial fix for making server side overlay building thread safe (thanks @kcdodd!) and some other small cleanups and additions to get into the 4.3.0 main release. diff --git a/.github/releases/v4.4.0.md b/.github/releases/v4.4.0.md new file mode 100644 index 00000000..c0976b0a --- /dev/null +++ b/.github/releases/v4.4.0.md @@ -0,0 +1,134 @@ +# Final release of 4.4.0 + +## Changes to node and profile subcommands + +Warewulf 4.4.0 unifies the underlying implementation of nodes and profiles to prevent divergence in their features and attributes going forward; but this necessitated an update to their respective command-line arguments to unify them as well. Changes to those arguments is detailed here: + +### wwctl node add + +* `--discoverable` becomes `--discoverable=true` + +### wwctl node set + +* `--addprofile` has been removed +* `--assetkey` becomes `--asset` +* `--delprofile` has been removed +* `--discoverable` becomes `--discoverable=true` +* `--netdeltag` becomes `--nettagdel` +* `--nettag` becomes `--nettagadd` +* `--system` becomes `--wwinit` +* `--tag` becomes `--tagadd` +* `--undiscoverable` becomes `--discoverable=false` + +### internal/app/wwctl/profile/set/root.go +* `--assetkey` becomes `--asset` +* `--discoverable` becomes `--discoverable=true` +* `--netdeltag` becomes `--nettagdel` +* `--nettag` becomes `--nettagadd` +* `--system` becomes `--wwinit` +* `--tag` becomes `--tagadd` +* `--undiscoverable` becomes `--discoverable=false` + +All changes introduced since v4.3.0 are summarized below. + +## Added + +- iPXE binaries included with Warewulf now support VLAN tagging. #563 +- `wwctl container list` now shows the container creation date, + modification date, and size. #537 +- `wwctl node edit` supports directly editing or defining node + configuration YAML in an editor. #540 +- `wwctl node export` and `wwctl node import` support importing and + exporting node definitions as YAML or (for import) CSV. The CSV file + must have a header in where the first field must always be the + nodename, and the rest of the fields are the same as the long + commandline options. Network device must have the form + `net.$NETNAME.$NETOPTION`. (e.g., `net.default.ipaddr`) #540 +- The `warewulfd.service` systemd unit file now supports `execreload` + and `execstop`. #550 +- Network interfaces now accept an `mtu` attribute. #549 +- The `wwinit` overlay now supports network interface configuration + via NetworkManager for Ethernet and InfiniBand interfaces. #539 +- Default node attribute values (e.g., for kernel arguments) are now + read in from a `defaults.conf` configuration file. If this file + is not present, built-in default values are used. #539 +- [Warewulf documentation](https://warewulf.org/docs/) is now managed + alongside the Warewulf source code in a single code repository so + that documentation may be updated alongside code changes. +- New man pages for `warewulf.conf` and `nodes.conf` #510 +- An initial cut of the [Warewulf API](API.md) #471 +- `wwctl show --render` shows overlay templates as they would be + rendered on a given target node. #467 +- `wwctl ssh` now supports Bash completion. #466 +- The environment variable `WW_CONTAINER_SHELL` is defined in a `wwctl + container shell` environment to indicate the container in use. #579 +- Network interface configuration (`ifcfg`) files now include the + interface name and type. #457 +- New `defaults.conf` man page. #593 +- A new debug overlay includes a template which demonstrates accessing + all available variables. #599 +- Distribute a README along with staticfiles. #189 +- Add a `-y` flag to `wwctl profile add`. #610 +- Distribute a source RPM with GitHub releases. #614 +- New Docker container node image for CentOS 7. #621 + +## Changed + +- `wwctl overlay edit` no longer saves a new template to the overlay + if the template is not modified from its initial state. #522 +- The wwinit overlay now only sets a name for a network interface if + that interface has a MAC address defined. #553 +- `wwctl container delete` now also deletes the built images + associated with that container. #214 +- Unified internal code paths for `wwctl profile` and `wwctl node` + commands, and between the on disk YAML format and the in memory + format, enabling the command-line options to be autogenerated from + the datastructures and ensuring that profile and node capabilities + remain in sync. Multiple command line arguments have been updated or + changed. #495, #637 +- `wwctl power` commands no longer separates node output with + additional whitespace. #514 +- No longer ask for confirmation when deleting 0 nodes. #603 +- Ask for confirmation during `wwctl container delete`. #606 + +## Fixed + +- `/etc/warewulf/excludes` (read from the node image) once again + excludes files from being included in the node image. #532 +- `wwctl ssh` always uses a node's primary interface. #544 +- `wwctl container show` now correctly shows the kernel version. #542 +- System users are no longer prevented from logging into compute + nodes. #538 +- `wwctl overlay chown` now correctly handles uid and gid arguments. #530 +- `wwctl overlay chown` no longer sets gid to `0` when unspecified. #531 +- Corrected the path for `.wwbackup` files in some situations. #524 +- Bypass `imgextract` for legacy BIOS machines to avoid 32-bit memory + limitations. #497 +- `warewulfd` no longer panics when network interface tags are + defined. #468 +- The wwinit overlay now configures the network device type. #465 +- Minor typographical fixes. #528, #519 +- Work-around for older versions of gzip that lack a `--keep` flag + during `wwctl container build`. #580 +- The default ipxe template is once again specified as a built-in + default and in `defaults.conf`. #581 +- `wwctl container list` no longer segfaults when a container chroot + is present without a built image. #585 +- `wwctl configure hostfile` now correctly detects the presence of the + hostfile overlay template. #571 +- `wwctl overlay build` no longer panics when rendering an template + for a node which has tags set. #568 +- Minor typographical fixes. #569 +- Directories within overlays no longer lose group/other write permissions #584 +- `wwctl profile set` now indicates "profiles" in output where it + previously mistakenly indicated "nodes." #600 +- Set correct overlay permissions for a NetworkManager configuration + file. #591 +- Replaced an invalid variable name in a NetworkManager overlay + template. #626 +- The 'nodes' alias now correctly refers to 'node' rather than + 'profile'. +- Fixed a typo in a log message. #631 +- Boolean attributes now correctly account for profile and default + values. #630 +- Kernel version is shown correctly for symlink'd kernels #640 diff --git a/.github/releases/v4.4.0rc1.md b/.github/releases/v4.4.0rc1.md new file mode 100644 index 00000000..fc27d0ed --- /dev/null +++ b/.github/releases/v4.4.0rc1.md @@ -0,0 +1,27 @@ +First release candidate of the 4.3.0 branch. + + +# Changes +## New features +- Changed internal interacation between the on disk YAML format and the in memory + format. This enables the command line options to be autogenerated from the datastructures. + Also this keeps the options for profiles and node set/add commands in strict sync. +- Added `node edit/export/import` where the edit command exposes the YAML config for the + given nodes in an editor. Nodes can also be added with this command. The export command + simply drops the YAML config for the given nodes. With the import command nodes can be + imported as YAML or as a CSV file. The CSV file must havr a header in where the first field + must always be the nodename, the rest of the fields are the same as the long commandline + options. Network device must have the form net.$NETNAME.$NETOPTION, which would e.g. be + net.default.ipaddr +- The default value (like kernel args) are no read in from the configuration file + default.conf. If this file couldn't be read built in default values will be used. +- Template files for NetworkManager are now present in the wwinit overlay. A interface must + have the type ethernet or infiniband that these will be brought up by NetworkManager +- Listings of container via `wwctl container list` will now show the creation,modification + date and the size. +- Initial cut of the warewulf API. See https://github.com/hpcng/warewulf/pull/471 +## Bug fixes +- For the connections of `wwctl ssh` the primary interface will be used +- Fixed kernel version in `wwctl container show` +- remove the container image and not only the rootfs dir on delete +- include and exclude handling for container were fixed diff --git a/.github/releases/v4.4.0rc2.md b/.github/releases/v4.4.0rc2.md new file mode 100644 index 00000000..71b03833 --- /dev/null +++ b/.github/releases/v4.4.0rc2.md @@ -0,0 +1,4 @@ +# Fixes +* The `ipxe template` did not have a default value so nodes did not boot if a this value was not set. Please check your `defaults.conf` if you installed this release +* older `gzip` versions are now handled correctly +* set WW_CONTAINER_SHELL to container name on `wwctl container exe` diff --git a/.github/releases/v4.4.0rc3.md b/.github/releases/v4.4.0rc3.md new file mode 100644 index 00000000..014dbaac --- /dev/null +++ b/.github/releases/v4.4.0rc3.md @@ -0,0 +1 @@ +Hopefully the last prerelease if no significant bugs are encountered. diff --git a/.github/releases/v4.4.1.md b/.github/releases/v4.4.1.md new file mode 100644 index 00000000..5db0e489 --- /dev/null +++ b/.github/releases/v4.4.1.md @@ -0,0 +1,7 @@ +Warewulf v4.4.1 is a bugfix release in the 4.4.x branch. This release backports certain fixes from the development branch. + +## Fixed + +- Container file gids are now updated properly during syncuser. #840 +- Added a missing `.ww` extension to the `70-ww4-netname.rules` template in the wwinit overlay. #724 +- Restrict access to `/warewulf/config` to root only. #728 diff --git a/.github/releases/v4.5.0.md b/.github/releases/v4.5.0.md new file mode 100644 index 00000000..0a775428 --- /dev/null +++ b/.github/releases/v4.5.0.md @@ -0,0 +1,130 @@ +The Warewulf project is pleased to announce the release of v4.5.0. + +There's a lot in this release! Some of the more prominent changes are described here. For a more complete list of changes, see the [changelog](https://github.com/warewulf/warewulf/blob/v4.5.0/CHANGELOG.md). + +## Testing, linting, and code formatting + +In this release we have significantly increased our focus on testing. Code changes, especially bug fixes, are increasingly accompanied by additional tests in the automated test suite, and this test suite is automatically run by GitHub against every pull request and every release. + +Use the `make test` target to run the test suite locally whenever you're doing development of your own. + +Additional Makefile targets have also been introduced (or improved) to aid in overall maintenance of the code base: + +- `make vet` uses standard golang heuristics to flag potential trouble +- `make fmt` applies a uniform source code format + +## Documentation + +Documentation continues to be an area for growth in Warewulf; but v4.5.x brings a number of improvements, some foundational: + +- v4.5.x documentation is now published separately, in addition to documentation for the `main` branch. +- The quickstart guides have been updated, particularly combining the guides for Enterprise Linux (e.g., Rocky Linux, CentOS, and RHEL). +- We've added a guide for using Vagrant as a development environment. + +There have been a few other specific additions: for example, there is now explicit documentation for how to configure multiple networks on a cluster node, and documentation for the "hostlist" syntax used by multiple `wwctl` commands. + +Please [reach out via Slack](https://warewulf.org/help) if you'd be interested in helping to improve the project's documentation! + +## Preview features + +This release brings three new features in a preview status: they haven't quite had enough use, testing, integration, or documentation to be ready for production, but they represent interesting new capabilities for the project and we are eager to hear about your experiences! + +### Initialize storage with Ignition + +You can now initialize file systems, partitions, and disks on attached storage during node boot using the Ignition utility. This does not yet support provisioning the node image to local disk; but it can be used to provision storage to be mounted into the node image; for example, to initialize a swap partition, to format a scratch disk, or to prepare local storage for system logs. + +[More information is available in the documentation.](https://warewulf.org/docs/v4.5.x/contents/disks.html) + +### Use dnsmasq for dhcp and tftp + +You can now use dnsmasq as a dhcp and tftp server in place of dhcpd and tftp-server. dnsmasq may become the default implementation in the future, unifying Warewulf on a single code-base for some of its down-stream services. + +[More information is available in the documentation.](https://warewulf.org/docs/v4.5.x/contents/dnsmasq.html) + +### Use GRUB as a node bootloader + +You can now use GNU GRUB as a node bootloader in place of iPXE. Using GRUB introduces support for secureboot, and may also help us to overcome some limitations on node image size that we have encountered with the available releases of iPXE. + +[More information is available in the documentation.](https://warewulf.org/docs/v4.5.x/contents/boot-management.html) + +## wwctl commands and arguments + +This release introduces multiple improvements to the `wwctl` utility. + +### New commands + +- `wwctl container copy` duplicates a container image to a new name, simplifying the process of testing a change in a test image. +- `wwctl container rename` adds support for renaming a container image in-place. (Nodes and profiles that refer to the old name must be updated separately.) + +### New command options and flags + +- `wwctl container syncuser --build` automatically (re)builds a container image after syncuser. +- `wwctl list --all` now only shows attributes that have a value set. + - `wwctl list --fullall` shows all attributes, including those which do not have a set value. +- `wwctl set --primarynet` sets the primary network device for the node, to be used for Warewulf provisioning. +- `wwctl set --ipmiescapechar` changes the `ipmitool` escape character. +- `wwctl overlay import --parents` automatically creates intermediate parent directories. + +### Fixes and improvements + +- `wwctl node edit` is now much more reliable, with multiple bugfixes and a general rewrite of its temporary file handling. +- `wwctl set --mtu` now correctly accepts valid MTU values. +- `wwctl container import` can now import a container archive using a relative path. +- `wwctl container import --force` no longer fails when replacing an existing container. +- `wwctl list` now supports a comma-separated list of nodes or profiles to filter the list to be displayed. +- `wwctl overlay` tab completion now supports completing files within an overlay. + +## Overlays + +Much of Warewulf's functionality is actually implemented using its bundled [overlays](https://warewulf.org/docs/v4.5.x/contents/overlays.html). + +- Nodes that use multiple profiles now include overlays from all associated profiles, applied in the order that the profiles are listed on the node (last wins). Individual overlays can be excluded using a `~` prefix. +- Built overlay images are no longer erroneously / confusingly mixed with overlay sources by default. (For example, overlay sources may be included in `/var/lib/warewulf/overlays`, and overlay images will now be built in `/var/lib/warewulf/provision/overlays`.) +- Built overlay images are now named either `__SYSTEM__` or `__RUNTIME__` by default, replacing the previous format that was a comma-separated list of the contained overlays. + +### wwinit + +- Network configuration files now correctly configure MTU. +- Access to `/warewulf/config` is now restricted to root only. +- Configuration of network device names is now more reliable, particularly for nodes with multiple interfaces. +- `ONBOOT` is now correctly configured when using `ifcfg`. +- Bonded network interfaces can now be configured via NetworkManager. + +### generic + +- The primary interface hostname is now used as the canonical name in `/etc/hosts`. +- The Warewulf server FQDN is now used as the canonical name in `/etc/hosts`. + +## Tags + +Warewulf suports arbitrary key-value tags on nodes, network devices, and ipmi interfaces. These tags can then be used by built-in and custom overlays. + +- Multiple bugs in the handling of node and profile tags have been fixed, making them a much more robust and reliable component of the Warewulf feature set. +- The `wwinit` overlay now uses `DNS` network tags (including `DNS1`, `DNS2`, etc.) to configure DNS resolution in network configuration files. + +## Configuration + +Warewulf is primarily configured using two configuration files, `warewulf.conf` and `nodes.conf`. + +### warewulf.conf + +- `ipaddr` now supports CIDR format, optionally inferring netmask and network. +- `paths` can be used to override compiled-in paths, e.g., for overlays, containers, and provisioned images. +- `tftp:ipxe` now supports full paths. + +All available configuration options can be seen in context using `wwctl genconfig warewulfconf print`. + +### nodes.conf + +- `PrimaryNetDev` now defines a single network interface as primary for a node (or profile), deprecating the `primary` boolean on each individual network device. + +## Build and packaging + +Finally, a number of improvements have been made to the Warewulf build system, including its Makefile and RPM specfile. + +- Warewulf no longer bundles an iPXE binary within its source code or packages, preferring, in stead, the version of iPXE included with the host disribution. + - A local, updated build of iPXE can still be built using an updated helper script at `scripts/build-ipxe.sh`. (Use `warwulf.conf:tftp:ipxe` to configure Warewulf to use a local iPXE build.) +- Warewulf can now be built on Fedora. +- New releases now include a build for Rocky Linux 9. +- New releases now include a build for OpenSUSE Leap 15.5 (replacing older versions of OpenSUSE Leap). +- A nightly release is now published on GitHub. diff --git a/.github/releases/v4.5.0rc1.md b/.github/releases/v4.5.0rc1.md new file mode 100644 index 00000000..7749cfcc --- /dev/null +++ b/.github/releases/v4.5.0rc1.md @@ -0,0 +1,3 @@ +Release candidate for upcoming release v4.5.0. + +For a complete list of changes, see https://github.com/warewulf/warewulf/blob/v4.5.0rc1/CHANGELOG.md diff --git a/.github/releases/v4.5.0rc2.md b/.github/releases/v4.5.0rc2.md new file mode 100644 index 00000000..7854e44b --- /dev/null +++ b/.github/releases/v4.5.0rc2.md @@ -0,0 +1,3 @@ +Second release candidate for upcoming release v4.5.0. + +For a complete list of changes, see https://github.com/warewulf/warewulf/blob/v4.5.0rc2/CHANGELOG.md diff --git a/.github/releases/v4.5.1.md b/.github/releases/v4.5.1.md new file mode 100644 index 00000000..527aa019 --- /dev/null +++ b/.github/releases/v4.5.1.md @@ -0,0 +1,34 @@ +Warewulf v4.5.1 is primarily a bugfix and dependency update release. + +Updates to dependencies broke compatibility with CentOS 7, so we have also dropped support for CentOS 7 (which, itself, goes EOL at the end of June) in this release. (This only affects the ability to run the Warewulf server on CentOS 7: Warewulf can still provision compute node images running CentOS 7.) + +# Added + +- Document warewulf.conf:paths. #635 +- New "Overlay" template variable contains the name of the overlay being built. #1052 + +# Changed + +- Update the glossary. #819 +- Upgrade the golang version to 1.20. +- Bump github.com/opencontainers/umoci to 0.4.7 +- Bump github.com/containers/image/v5 to 5.30.0 +- Bump github.com/docker/docker to 25.0.5+incompatible +- Bump github.com/go-jose/go-jose/v3 to 3.0.3 +- Bump gopkg.in/go-jose/go-jose.v2 to 2.6.3 +- Bump github.com/opencontainers/runc to 1.1.12 +- Dynamically calculate version and release from Git. #1162 +- Update quickstarts to configure firewalld for dhcp. #1133 +- Omit building the API on EL7. #1171 +- Syncuser only walks the file system if it is going to write. #1207 + +# Fixed + +- Fix `wwctl profile list -a` format when kernerargs are set. +- Don't attempt to rebuild protocol buffers in offline mode. #1155 +- Fix Suse package by moving yq command to `%install` section. #1169 +- Fix a rendering bug in the documentation for GRUB boot support. #1132 +- Fix a locking issue with concurrent read/writes for node status. #1174 +- Fix shim and grub detection for aarch64. #1145 +- wwctl [profile|node] list -a now handles slices correclty. #1113 +- Fix parsing of /etc/group during syncuser. #1202 diff --git a/.github/releases/v4.5.2.md b/.github/releases/v4.5.2.md new file mode 100644 index 00000000..93674695 --- /dev/null +++ b/.github/releases/v4.5.2.md @@ -0,0 +1,14 @@ +Warewulf v4.5.2 is primarily a security fix release to address CVE-2024-3727. + +Other fixes and changes already staged for v4.5.x are also included. The full changelog is below. + +# Added + +- Allow specification of the ssh-keys to be to be created. #1185 + +# Fixed + +- Fix nightly release build failure issue. #1195 +- Reorder dnsmasq config to put iPXE last. #1146 +- Update a reference to `--addprofile` to be `--profile`. #1085 +- Update a dependency to address CVE-2024-3727. #1221 diff --git a/.github/releases/v4.5.3.md b/.github/releases/v4.5.3.md new file mode 100644 index 00000000..cd3f1a59 --- /dev/null +++ b/.github/releases/v4.5.3.md @@ -0,0 +1,34 @@ +Warewulf v4.5.3 includes additional functionality, documentation, usability improvements, and bugfixes. This includes an initial preview of 2-stage boot support based on dracut; significant efficiency improvements for "syncuser"; a remedy for a regression that affected the generation of ssh host keys during initial configuration; and improved handling of temporary files generated during container shell and exec commands. + +# Added + +- Initial support for dracut-based 2-stage boot. See https://warewulf.org/docs/v4.5.x/contents/boot-management.html#booting-with-dracut for more information. + - Add `stage=initramfs` to warewulfd provision to serve initramfs from container image. #1115 + - Add `warewulf-dracut` package to support building Warewulf-compatible initramfs images with dracut. #1115 + - Add iPXE template `dracut.ipxe` to boot a dracut initramfs. #1115 + - Add dracut menuentry to `grub.cfg.ww` to boot a dracut initramfs. #1115 + - Add `.NetDevs` variable to iPXE and GRUB templates, similar to overlay templates. #1115 + - Add `.Tags` variable to iPXE and GRUB templates, similar to overlay templates. #1115 + +# Changed + +- `wwctl container import` now only runs syncuser if explicitly requested. #1212 +- Improved detection of SELinux capable root fs #1093 + +# Fixed + +- Block unprivileged requests for arbitrary overlays in secure mode. #1215 +- Fix the issue that warewulf.conf parse does not support CIDR format. #1130 +- Reduce the number of times syncuser walks the container file system. #1209 +- Create ssh keys when calling `wwctl configure --all` #1250 +- Create temporary overlayfs directories alongside container rootfs and remove them after use #1180 + +# Documentation + +- Add examples for building overlays in parallel to documentation. (https://warewulf.org/docs/v4.5.x/contents/overlays.html#build) +- Replace reference to docusaurus with Sphinx (https://warewulf.org/docs/v4.5.x/contributing/documentation.html#documentation) +- Fix installation docs to use github.com/warewulf instead of github.com/hpcng. #1219 (https://warewulf.org/docs/v4.5.x/contents/installation.html) + +# Dependencies + +- Bump golang.org/x/net from 0.22.0 to 0.23.0. #1223 diff --git a/.github/releases/v4.5.4.md b/.github/releases/v4.5.4.md new file mode 100644 index 00000000..18b1e06b --- /dev/null +++ b/.github/releases/v4.5.4.md @@ -0,0 +1,5 @@ +Warewulf v4.5.4 fixes a regression that was introduced in v4.5.3 that broke argument parsing for `wwctl container shell` and `wwctl container exec`. + +# Fixed + +- Fix a regression that caused an error when passing flags to `wwctl container exec` and `wwctl container shell`. #1250 diff --git a/.github/releases/v4.5.5.md b/.github/releases/v4.5.5.md new file mode 100644 index 00000000..5709e82c --- /dev/null +++ b/.github/releases/v4.5.5.md @@ -0,0 +1,23 @@ +Warewulf v4.5.5 includes usability improvements and bugfixes for the v4.5.x line. This includes the restoration of previous behavior when running DHCP in "static" mode, reliability improvements for `wwctl overlay edit`, and fixes for overlay autobuild. + +# Known issues + +- The fix for overlay autobuild mistakenly autobuilds empty overlays. We recommend manually disabling autobuild in `warewulf.conf` in v4.5.5. Fixed in #1301. + +# Fixed + +- Support leading and trailing slashes in `/etc/warewulf/excludes`. #1266 +- Fix a regression in overlay autobuild. #1216 +- Fix wwclient not reading asset-tag. #1110 +- Fix dhcp not passing asset tag or uuid to iPXE. #1110 +- Restored previous static dhcp behavior. #1263 +- Capture "broken" symlinks during container build. #1267 +- Fix the issue that removing lines during wwctl overlay edit didn't work. #1235 +- Fix the issue that new files created with wwctl overlay edit have 755 permissions. #1236 +- Fix tab-completion for `wwctl overlay list`. #1260 + +# Changed + +- Explicitly ignore compat-style NIS lines in passwd/group during syncuser. #1286 +- Accept `+` within kernel version. #1268 +- Mount `/sys` and `/run` during `wwctl container exec`. #1287 diff --git a/.github/releases/v4.5.6.md b/.github/releases/v4.5.6.md new file mode 100644 index 00000000..1414454a --- /dev/null +++ b/.github/releases/v4.5.6.md @@ -0,0 +1,22 @@ +Warewulf v4.5.6 includes a significant fix for overlay autobuild, resolving a bug in v4.5.5 that caused autobuilt overlays to be empty. We've also added tab completion for the remaining subcommands of `wwctl overlay`, and simplified the `wwctl container list` command to break-out size calculations as optional and only report one type of size at a time. (Previously, container size was reported as the sum of the chroot, the uncompressed archive, and the compressed archive.) + +The full changelog is below: + +# Added + +- Show more information during `wwctl container ` about when and if the container image will be rebuilt. #1302 +- Command-line completion for `wwctl overlay `. #1298 +- Display an error during boot if no container is defined. #1295 +- `wwctl conatiner list --kernel` shows the kernel detected for each container. #1283 +- `wwctl container list --size` shows the uncompressed size of each container. `--compressed` shows the compressed size, and `--chroot` shows the size of the container source on the server. #954, #1117 +- Add a logrotate config for `warewulfd.log`. #1311 + +# Fixed + +- Ensure autobuilt overlays include contextual overlay contents. #1296 +- Fix the failure when updating overlay files existing on different partitions. #1312 +- Escape asset tag for `wwclient` query strings when pulling runtime overlays. #1310 + +# Changed + +- `wwctl container list` only lists names by default. (`--long` shows all attributes.) #1117 diff --git a/.github/releases/v4.5.7.md b/.github/releases/v4.5.7.md new file mode 100644 index 00000000..467ca492 --- /dev/null +++ b/.github/releases/v4.5.7.md @@ -0,0 +1,36 @@ +Warewulf v4.5.7 fixes the ability to override overlay files configured in profiles with overlays configured per-node; fixes a template processing bug bug in development-time overlay rendering; and improves the preview dracut-based boot process to better support a "secure" boot process. + +It also adds the ability to temporarily copy files into containers for possible modification during `wwctl container exec` and `wwctl container shell.` + +**Dracut booting in v4.5.7 breaks compatibility with earlier versions of `warewulf-dracut`.** To continue using dracut booting with Warewulf v4.5.7, update `warewulf-dracut` in the container image and re-build an initramfs inside the container. ([docs][1]) + +[1]: https://warewulf.org/docs/v4.5.x/contents/boot-management.html#booting-with-dracut + +Contributors: + +* Josh Burks (Arizona State University) +* Jonathon Anderson (CIQ) +* Elmar Pruessee (National Jewish Health) +* Christian Goll (Suse) +* "Jason" Xu Yang (CIQ) +* Tobias Poschwatta (Zuse Institute Berlin) + +The full changelog is below: + +# Added + +- Added option for wwclient port number. #1349 +- Additional helper directions during syncuser conflict. #1359 +- Add `:copy` suffix to `wwctl container exec --bind` to temporarily copy files into the node image. #1365 + +# Changed + +- Added a link to an example SELinux-enabled node image in documentation. #1305 +- Refine error handling for `wwctl configure`. #1273 +- Updated dracut guidance for building initramfs. #1369 + +# Fixed + +- Fixed application of node overlays such that they override overlapping files from profile overlays. #1259 +- Prevent overlays from being improperly used as format strings during `wwctl overlay show --render`. #1363 +- Fix dracut booting with secure mode. #1261 diff --git a/.github/releases/v4.5.8.md b/.github/releases/v4.5.8.md new file mode 100644 index 00000000..67944532 --- /dev/null +++ b/.github/releases/v4.5.8.md @@ -0,0 +1,45 @@ +Warewulf v4.5.8 simplifies the "wwinit" boot process for SELinux and configures tmpfs to spread the node image across all available NUMA nodes. It also improves the detection of kernels in the container image to more reliably detect the newest available kernel and to avoid debug / rescue kernels. + +This release also includes additional bug fixes and documentation improvements. + +Contributors for this release: + +- "Jason" Xu Yang (CIQ) +- Elmar Pruesse (National Jewish Health) +- Tobias Ribizel +- Jonathon Anderson (CIQ) +- Christian Goll (Suse) +- Brandon Biggs (INL) + +The full changelog is included below. + +# Known issues + +- [wwctl container import behaves differently for directories than for images](https://github.com/warewulf/warewulf/issues/1447) + +# Added + +- Added `--syncuser` flag to `wwctl container shell`. #1358 +- Added a troubleshooting guide. #1234 +- Added documentation about `rootfstype=ramfs` for SELinux support. #1001 +- Added workaround documentation for importing containers with sockets. #892 +- Added documentation for building iPXE locally. #1114 +- Documented that ignition is not available for Rocky Linux 8. #1373, #1272 +- Additional help text when container `RunDir` already exists. #1389 + +# Changed + +- Interleave tmpfs across all available NUMA nodes. #1347, #1348 +- Syncuser watches for changes in mtime rather than ctime. #1358 +- Change the default permissions for provisioned overlay images to `0750` (dirs) and `0660` (files). #1388 + +# Fixed + +- Return an error during `wwctl container import` if the archive filename includes a colon. #1371 +- Correctly extract smbios asset key during GRUB boot. #1291 +- Refactor of `wwinit/init` to more properly address rootfs options. #1098 +- Fix autodetected kernel sorting and filtering. #1332 +- Avoid a panic during container import. #1244 +- Make sure that tftp files have unmasked permissions at creation time. #674 +- Fix "onboot" behavior for NetworkManager, Debian networking, and Suse wicked. #1278 +- Clarified missing steps in Enterprise Linux quickstart. #1179 diff --git a/.github/releases/v4.6.0.md b/.github/releases/v4.6.0.md new file mode 100644 index 00000000..ba779d68 --- /dev/null +++ b/.github/releases/v4.6.0.md @@ -0,0 +1,22 @@ +> [!IMPORTANT] +> This release has been restored after having been erroneously deleted from GitHub. The attached assets have been rebuilt from the original source. Behavior should be 100% identical; but you may notice that package checksums/hashes have changed. +> +> We are investiating methods for preventing this from happening in the future, and apologize for any confusion. + +Warewulf v4.6.0 is a significant upgrade, with many changes relative to the v4.5.x series: + +- new configuration upgrade system +- changes to the default profile +- renaming containers to (node) images +- new kernel management system +- parallel overlay builds +- sprig functions in overlay templates +- improved network overlays +- nested profiles +- arbitrary "resources" data in nodes.conf +- NFS client configuration in nodes.conf +- emphatically optional syncuser +- improved network boot observability +- movements towards Debian/Ubuntu support + +Particularly significant changes, especially those affecting the user interface, are described in the [release notes](https://warewulf.org/docs/v4.6.x/release/v4.6.0.html). Additional changes not impacting the user interface are listed in the [CHANGELOG](https://github.com/warewulf/warewulf/blob/main/CHANGELOG.md). diff --git a/.github/releases/v4.6.0rc1.md b/.github/releases/v4.6.0rc1.md new file mode 100644 index 00000000..2b8cb9a4 --- /dev/null +++ b/.github/releases/v4.6.0rc1.md @@ -0,0 +1,194 @@ +Warewulf v4.6.0rc1 is the first release candidate for the upcoming v4.6.0. Warewulf v4.6.0 is a major release that includes many significant enhancements, fixes, and changes. + +- rename "containers" to "images" +- "sprig" functions in overlay templates +- support for yaml and json formatted output +- `wwctl upgrade` to automatically update `nodes.conf` and `warewulf.conf` +- completely re-designed kernel selection support +- nested profiles +- arbitrary node and profile data in new "resources" structure +- moved NFS mount options to resources / fstab overlay +- split overlays by function +- split overlays, site vs distribution +- replaced `defaults.conf` with settings on default profile +- improved tabular output +- parallel overlay build +- improved networking functionality (static route, vlans, and bonds) +- kernel arguments as a list +- non-zero exit codes on wwctl errors +- fixed argument parsing for `wwctl image exec` + +This release also includes numerous additional bug fixes and other. + +Contributors for this release: + +- Adam Michel +- Arian +- Benedikt Braunger +- Beni +- Brandon Biggs +- Brian Clemens (CIQ) +- Christian Goll (Suse) +- Daniele Colombo +- David McFarlane +- Dietmar Rieder +- Elmar Pruesse +- John "griznog" Hanks +- Howard Van Der Wal (CIQ) +- Ian Kaufman +- Josh Burks (ASU) +- Jonathon Anderson (CIQ) +- Nicholas Porter +- Shane Nehring (Iowa State University) +- Stephen Simpson (CIQ) +- Timothy Middelkoop (internet2) +- Tobias Poschwatta +- Tobias Ribizel +- "Jason" Xu Yang +- Yong Qin (Nividia) + +The full changelog is included below. + +# Added + +- Added Netplan NIC support for Debian/Ubuntu #1463 +- Added documentation on ensuring `systemctl restart warewulfd` is ran when editing `nodes.conf` or `warewulf.conf` +- Add the ability to boot nodes with `wwid=[interface]`, which replaces + `interface` with the interface MAC address +- Added https://github.com/Masterminds/sprig functions to templates #1030 +- Add multiple output formats (yaml & json) support. #447 +- More aliases for many wwctl commands +- Add support to render template using `host` or `$(uname -n)` as the value of `overlay show --render`. #623 +- Added command line parameters for credentials of a container registry +- Add flag `--build` to `wwctl container copy`. #1378 +- Add `wwctl clean` to remove OCI cache and overlays from deleted nodes +- Add `wwctl container import --platform`. #1381 +- Read environment variables from `/etc/default/warewulfd` #725 +- Add support for VLANs to NetworkManager, wicked, ifcfg, debian.network_interfaces overlays. #1257 +- Add support for static routes to NetworkManager, wicked, ifcfg, debian.network_interfaces overlays. #1257 +- Add `wwctl upgrade `. #230, #517 +- Better handling of InfiniBand udev net naming. #1227 +- use templating mechanism for power commands. #1004 +- Document "known issues." +- Add `wwctl --kernelversion` to specify the desired kernel version or path. #1556 +- Add `wwctl container kernels` to list discovered kernels from containers. #1556 +- Add possibility to define a softlink target with an overlay template +- Support defining a symlink with an overlay template. #1303 +- New "localtime" overlay to define the system time zone. #1303 +- Add support for nested profiles. #1572, #1598 +- Adds `wwctl container --build=false` to prevent automatically (re)building the container. #1490, #1489 +- Added resources as generic, arbitrary YAML data for nodes and profiles. #1568 +- New `fstab` resource configures mounts in fstab overlay, including NFS mounts. #515 +- Add Dev Container support #1653 +- Add man pages and command reference to userdocs. #1488 +- Document building images from scratch with Apptainer. #1485 +- Added warewulfd:/overlay-file/{overlay}/{path...}?render={id} + +# Changed + +- Renamed "container" to "image" throughout wwctl and overlay templates. #1385 +- Locally defined `tr` has been dropped, templates updated to use Sprig replace. +- Bump github.com/opencontainers/image-spec to 1.1.0 +- Bump google.golang.org/grpc 1.62.1 +- Bump google.golang.org/protobuf to 1.33.0 +- Bump github.com/containers/image/v5 to 5.30.0 +- Bump github.com/docker/docker to 25.0.5+incompatible +- Bump github.com/grpc-ecosystem/grpc-gateway/v2 from 2.18.0 to 2.19.1 #1165 +- Bump github.com/spf13/cobra from 1.7.0 to 1.8.0 #1166 +- Bump github.com/fatih/color from 1.15.0 to 1.17.0 #1224 +- Bump github.com/coreos/ignition/v2 from 2.15.0 to 2.19.0 #1239 +- Bump github.com/spf13/cobra from 1.8.0 to 1.8.1 #1481 +- Bump google.golang.org/protobuf from 1.34.1 to 1.35.1 #1480 +- Bump golang.org/x/term from 0.20.0 to 0.25.0 #1476 +- Bump github.com/grpc-ecosystem/grpc-gateway/v2 from 2.19.1 to 2.23.0 #1513 +- Bump github.com/containers/image/v5 from 5.30.1 to 5.32.2 #1366 +- Bump github.com/fatih/color from 1.17.0 to 1.18.0 #1523 +- Disable building containers by default when calling `wwctl container copy`. #1378 +- Split wwinit and generic overlays into discrete functionality. #987 +- Updated IgnitionJson to sort filesystems. #1433 +- `wwctl node set` requires mandatory pattern input. #502 +- Remove NodeInfo (in-memory-only) data structure, consolidating onto NodeConf. #916 +- Replace `defaults.conf` with settings on the default profile. #917 +- Switched from yaml.v2 to yaml.v3 #1462 +- Make OCIBlobCache a seperate path and point it to `/var/cache` #1459 +- Updated various shell scripts for POSIX compatibility. #1464 +- Update `wwctl server` to always run in the foreground #508 +- Update `wwctl server` to log to stdout rather than a file #503 +- Changed `wwctl server` to use "INFO" for send and receive logs #725 +- Remove a 3-second sleep during iPXE boot. #1500 +- Don't package the API in RPM packages by default. #1493 +- Update default `warewulfd` port to match shipped configuration. #1448 +- Replace `olekukonko/tablewriter` with `cheynewallace/tabby`. #1497, #1498 +- replaced deprecated errors.Wrapf with fmr.Errorf. #1534 +- Rename udev net naming file to 70-persistent-net.rules. #1227 +- Manage warewulfd template data as a pointer. #1548 +- Added test for sending grub.cfg.ww. #1548 +- Use a sentinel file to determine container readonly state. #1447 +- Bump github.com/Masterminds/sprig/v3 from 3.2.3 to 3.3.0 #1553 +- Bump github.com/golang/glog from 1.2.0 to 1.2.3 #1527 +- Bump github.com/opencontainers/runc from 1.1.12 to 1.1.14 +- Repurpose Kernel.Override to specify the path to the desired kernel within the container. #1556 +- Merge Kernel.Override into Kernel.Version to specify the desired kernel version or path. #1556 +- Provide detected kernel version to overlay templates. #1556 +- Bump github.com/containers/storage from 1.53.0 to 1.55.2 #1316, #892 +- Process nodes.conf path dynamically from config. #1595, #1596, #1569 +- Split overlays into distribution and site overlays. #831 +- Added note to booting userdoc for removing machine-id. #1609 +- Log cpio errors more prominently. #1615 +- Improved syncuser conflict help text. #1614 +- Parallelized overlay build. #1018 +- Parallelized and optimized overlay build. #1018 +- Added note about dnsmasq interface options in Rocky 9. +- Added retries to curl in wwinit dracut module. #1631 +- Added ip= argument to dracut ipxe script. #1630 +- Updated network interface bonding configuration and documentation. #1482, #1280 +- Refactor Kernel arguments as a slice (list) rather than a single string. #1656 + +# Removed + +- `wwctl node list --fullall` has been removed +- `wwctl profile list --fullall` has been removed +- Remove `wwctl server ` #508 +- Remove `wwctl overlay build --host` #1419 +- Remove `wwctl overlay build --nodes` #1419 +- Remove `wwctl kernel` #1556 +- Remove `wwctl --kerneloverride` #1556 +- Remove `wwctl container --setdefault` #1335 +- Remove NFS mount options from warewulf.conf. #515 + +# Fixed + +- Update links on contributing page to point to warewulf repo. +- Prevent Networkmanager from trying to optain IP address via DHCP + on unused/unmanaged network interfaces. +- Systems with no SMBIOS (Raspberry Pi) will create a UUID from + `/sys/firmware/devicetree/base/serial-number` +- Replace slice in templates with sprig substr. #1093 +- Fix an invalid format issue for the GitHub nightly build action. #1258 +- Return non-zero exit code on overlay build failure #1393 +- Return non-zero exit code on container copy failure #1377 +- Return non-zero exit code on container sub-commands #1414 +- Fix excessive line spacing issue when listing nodes. #1241 +- Return non-zero exit code on node sub-commands #1421 +- Fix panic when getting a long container list before building the container. #1391 +- Return non-zero exit code on power sub-commands #1439 +- Fix issue that pattern matching broken on `node set` #964 +- Fix issue that domain globs not supported during wwctl node delete. #1449 +- Fix overlay permissions in /root/ and /root/.ssh/. #1452 +- Return non-zero exit code on container sub-commands #1437 +- Return non-zero exit code on profile sub-commands #1435 +- Fix issue that NetworkManager marks managed interfaces "unmanaged" if they do + not have a device specified. #1154 +- Return non-zero exit code on overlay sub-commands #1423 +- Simplify passing of arguments to commands through `wwctl container exec`. #253 +- Don't update IPMI if password isn't set. #638 +- Fix issue that `--nettagdel` does not work properly. #1503 +- Fix test for dhcp static configuration #1536 #1537 +- Fix issue that initrd fails at downloading runtime overlay with permission denied error, + when warewulf secure option in warewulf.conf is enabled. #806 +- Allow iPXE to continue booting without runtime overlay. #806 +- Format errors in logs as strings. #1563 +- Fix display of profiles during node list. #1496 +- Fix internal DelProfile function to correctly operate on profiles rather than nodes. #1622 +- Fix parsing of bool command line variables #1627 +- Fix newline handling in /etc/issue. #1648 diff --git a/.github/releases/v4.6.0rc2.md b/.github/releases/v4.6.0rc2.md new file mode 100644 index 00000000..058a4243 --- /dev/null +++ b/.github/releases/v4.6.0rc2.md @@ -0,0 +1,54 @@ +Warewulf v4.6.0rc2 is the second release candidate for the upcoming v4.6.0. Warewulf v4.6.0 is a major release that includes many significant enhancements, fixes, and changes. + +This second release candidate fixes many additional bugs and makes additional expected changes. The full changelog is included below. + +For more information regarding changes in v4.6.0, see the [v4.6.0rc1](https://github.com/warewulf/warewulf/releases/tag/v4.6.0rc1) release. + +# Added + +- Document defining kernel args that include commas. #1679 +- Recommend installing ipmitool with Warewulf package. #970 +- Add completion for profile list. #1695 +- Add OPTIONS argument for `warewulfd.service`. #1707 +- Document `warewulf.conf:dhcp.template`. #1701 +- New template field `IpCIDR`. #1700 +- `wwctl configure` persists auto-detected server network settings to `warewulf.conf`. #1700 +- Run staticcheck as part of GitHub CI. #1657 + +# Changed + +- `wwctl node list <--yaml|--json>` outputs a map keyed by node name. #1667 +- Don't mount /run during wwinit. #1566 +- Simpler permissions in official RPM packages. #1696 +- Only calculate image chroot size when requested. #1504 +- Create temporary files in overlay directory during `wwctl overlay edit`. #1473 +- Re-order SSH key types to make ed25519 default. #981 +- Don't assume default values for `warewulf.conf` network settings. #1700 +- Omit DHCP pool from `dhcpd.conf` if any required fields are missing. #1700 +- `warewulf.conf:ipaddr6` is no longer required to be a `/64` or smaller. #1700 + +# Fixed + +- Fix default nodes.conf to use the new kernel command line list format. #1670 +- Fix `make install` when `sudo` does not set `$PWD`. #1660 +- Use sh to parse and exec IPMI command. #1663 +- Use configured warewulf.conf path in `wwctl upgrade`. #1658 +- Fixed negation for slice field elements during profile/node merge. #1677 +- Show each overlay only once, even when both site and distribution versions exist. #1675 +- Remove a redundant "Building image" log message after image exec. #1694 +- Don't populate NetDevs[].Type or NetDevs[].Netmask during upgrade. #1661 +- Prefer parent profile values over child profile values. #1672 +- Don't attempt to back-up an output file that doesn't exist during upgrade. #1671 +- Specify init=/init when booting with Grub+dracut. #1573 +- Fix a warewulfd panic when no kernel fields are specified. #1689 +- Create site overlay directory. #1690 +- Urlencode asset keys during dracut boot. #1610 +- Set execute permissions for intermediate directories during `wwctl overlay import --parents`. #1655 +- Fix log output formatting during overlay build. +- Prevent merging of zero-value net.IP fields. #1710 +- Properly handle parsing of server network and netmask from CIDR `warewulf.conf:ipaddr`. #1541, #1594 +- Populate template field `NetworkCIDR`. #1700 + +# Removed + +- Remove `warewulf.conf:syslog`. #1606 diff --git a/.github/releases/v4.6.0rc3.md b/.github/releases/v4.6.0rc3.md new file mode 100644 index 00000000..eb355faa --- /dev/null +++ b/.github/releases/v4.6.0rc3.md @@ -0,0 +1,63 @@ +Warewulf v4.6.0rc3 is the third release candidate for the upcoming v4.6.0, and is expected to be the last release candidate before the final release. Warewulf v4.6.0 is a major release that includes many significant enhancements, fixes, and changes. + +This third release candidate fixes many additional bugs and makes additional expected changes. The full changelog is included below. + +For more information regarding changes in v4.6.0, see the [v4.6.0rc1](https://github.com/warewulf/warewulf/releases/tag/v4.6.0rc1) and [v4.6.0rc2](https://github.com/warewulf/warewulf/releases/tag/v4.6.0rc2) releases. + +# Added + +- Added missing hostlist support for `wwctl node` and `wwctl overlay build`. #1635 +- Added support for comma-separated hostlist patterns. #1635 +- Added default value for `warewulf.conf:dhcp.template`. #1725 +- Added `UniqueField` template function. #829 +- Added `wwctl image build --syncuser`. #1321 +- Added support for a DNSSEARCH netdev tag in network configuration overlays. #1256 +- Added `WW_HISTFILE` to control shell history location during `wwctl image shell`. #1732 +- Added target help in Makefile. #1740 +- Added fstab mounts for `/home` and `/opt` to initial default profile. #1744 +- Add support for an `IPXEMenuEntry` tag to select the boot method during iPXE. + +# Changed + +- Hide internal `wwctl completion` and `wwctl genconfig` commands. #1716 +- Make .ww suffix optional during `wwctl overlay show --render`. #649 +- DHCP template generates as much of the subnet and range definition as possible. #1469 +- Updated overlay flags to `wwctl [--runtime-overlays|--system-overlays]`. #1495 +- syncuser overlay reads host passwd and group database from sysconfdir. #1736 +- syncuser overlay skips duplicate users and groups in passwd and group databases. #829 +- `wwctl image syncuser --write` is true by default. #1736 +- Update syncuser documentation. #1736 +- Update PS1 during `wwctl image shell` to include working directory by default, + and to include `PS1` from the environment if present. #1245 +- DHCP template generates as much of the subnet and range definition as possible. #1469 +- Updated overlay flags to `wwctl [--runtime-overlays|--system-overlays]`. #1495 +- Added logging and updated output during iPXE and GRUB. #1156 +- Defined a menu for iPXE. #1156 +- Added logging to wwinit scripts. #1156 +- Renamed /warewulf/wwinit to /warewulf/prescripts. #1156 +- Display auto-detected kernel version during iPXE and GRUB. #1742 +- Reduced default verbosity of `wwctl overlay build`. + +# Fixed + +- Fixed detection of overlay files in `wwctl overlay list --long`. +- Fixed panics in `wwctl node sensors` and `wwctl node console` when ipmi not configured. +- Fixed completions for `wwctl` commands. +- Return "" when NetDev.IpCIDR is empty. +- Updated `wwctl node export` to include node IDs. #1718 +- Don't add "default" profile to new nodes if it does not exist. #1721 +- Make DHCP range optional. +- Don't use DHCP for interfaces attached to a bond. #1743 +- Wait until ignition has completed before trying to mount. +- Fix timeout problem for wwclient. #1741 +- Fixed default "true" state of NetDev.OnBoot. #1754 +- Port NFS mounts during `wwctl upgrade nodes` before applying defaults. #1758 + +# Removed + +- Removed partial support for regex searches in node and profile lists. #1635 +- Remove redundant `wwctl genconfig completions` command. #1716 +- Remove syncuser warning messages in `wwctl` that assume its use. #1321 +- Remove syncuser from the list of default runtime overlays. #1322 +- Removed check for "discoverable" profiles during `wwctl upgrade nodes`. +- Removed `dracut.ipxe` template. (Use `default.ipxe` and set tag `IPXEMenuEntry=dracut`.) diff --git a/.github/releases/v4.6.1.md b/.github/releases/v4.6.1.md new file mode 100644 index 00000000..9e72eb6b --- /dev/null +++ b/.github/releases/v4.6.1.md @@ -0,0 +1,16 @@ +Warewulf v4.6.1 is a regularly-scheduled minor release in the v4.6.x series. + +- aarch64 packages +- new REST API +- changes to JSON output +- new command-line options +- new IPMI tag to configure interface vlan +- network configuration for `wwclient` +- New sos plugin +- fixes to `wwctl upgrade` and default settings +- other misc. bug fixes + +Significant changes are detailed in the [release +notes](https://warewulf.org/docs/v4.6.x/release/v4.6.1.html). Additional changes +may be listed in the +[CHANGELOG](https://github.com/warewulf/warewulf/blob/main/CHANGELOG.md). diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 9bb395a2..7d0ff335 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -2,6 +2,7 @@ name: check on: + workflow_dispatch: {} push: branches: - main diff --git a/.github/workflows/nightly-release.yml b/.github/workflows/nightly-release.yml index 473c52dd..2da3da77 100644 --- a/.github/workflows/nightly-release.yml +++ b/.github/workflows/nightly-release.yml @@ -2,7 +2,7 @@ name: nightly on: - workflow_dispatch: + workflow_dispatch: {} schedule: - cron: '05 00 * * *' diff --git a/.github/workflows/tagged-release.yml b/.github/workflows/tagged-release.yml index 1279163f..7110c6db 100644 --- a/.github/workflows/tagged-release.yml +++ b/.github/workflows/tagged-release.yml @@ -2,10 +2,13 @@ name: tagged-release on: + workflow_dispatch: {} push: tags: - 'v*.*.*' +permissions: write-all + jobs: dist: name: Checkout code and build warewulf.spec and dist @@ -75,8 +78,14 @@ jobs: - name: Build RPMs and run tests run: | - mock -r ${{ matrix.target }}-${{ matrix.arch }} --rebuild --spec=warewulf.spec --sources=. \ - && mock -r ${{ matrix.target }}-${{ matrix.arch }} --chroot -- bash -c "make -C /builddir/build/BUILD/warewulf-*/ test" + root="${{ matrix.target }}-${{ matrix.arch }}" + eol_root="/etc/mock/eol/${root}.cfg" + if [ -f "${eol_root}" ] + then + root="${eol_root}" + fi + mock --root="${root}" --rebuild --spec=warewulf.spec --sources=. \ + && mock --root="${root}" --chroot -- bash -c "make -C /builddir/build/BUILD/warewulf-*/ test" - name: Attach RPM and SRPM to release uses: xresloader/upload-to-github-release@v1 diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ca41d7..db976c87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). -## v4.6.1, unreleased +## v4.6.1, 2025-04-04 ### Added - Added `wwctl overlay import --overwrite` to overwrite existing overlay file. -- Add `--retry-connrefused` to dracut curl commands. +- wwclient uses `WW_IPADDR`, if set, to contact the Warewulf server. #1788 +- Add `wwctl node import --yes` to assume yes to confirmations. +- Set an IPMI tag ``vlan`` to configure the vlan during ``ipmiwrite``. #1031 +- Added net.ifnames=1 to default kernel argument list. #1820 +- Add a new OpenAPI v3 REST API to warewulfd at /api. #1588 +- New sos plugin in `warewulf-sos` subpackage. #1822 +- Add `--retry-connrefused` to dracut curl commands. #1800 ### Fixed @@ -19,10 +25,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Properly configure a default IPMI template during `wwctl upgrade nodes --add-defaults`. - Fixed display of dotted field names. #1825 - Add single quote around escapechar in ipmitool template #1830 +- Update nodes and profiles when renaming an image. #1637 +- If init is not specified, wwinit looks for /sbin/init, /etc/init, and /bin/init. #1808 +- assume ipxe template=default if not set. #1808, #1813 +- Display a warning during overlay build if an overlay list is empty. #1808 +- Fix processing of UNDEF and UNSET during `wwctl set`. #1837 +- Actually cause grub to sleep and reboot when log messages indiacte. #1838 +- Fixed issue with importing new nodes from yaml. #1842 +- `wwctl upgrade nodes --replace-overlays` avoids applying overlays multiple times. #1823 +- Fix a panic during `wwctl node list --ipmi` for nodes with no ipmi configuration. #1847 +- Allow `--verbose` and `--debug` to set log level for warewulfd. #1801 ### Changed - Update GitHub actions to build aarch64 artifacts. +- Explicitly enforce the number or arguments accepted by some `wwctl` subcommands. #1717 +- Renamed `wwctl node import --cvs` to `--csv`. +- Update JSON output to match YAML field names. #1588 +- Update JSON output to omit empty values. #1588 ### Removed @@ -38,7 +58,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Added - Support Ubuntu-style dracut initrd images. -- New sos plugin in `warewulf-sos` subpackage. ### Fixed diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index a501faff..70ff0b5f 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -44,4 +44,5 @@ * Nicholas Porter * Ian Kaufman [@iankgt40](https://github.com/iankgt40) * Daniele Colombo [@dacolombo](https://github.com/dacolombo) -* Stephen Simpson [@ssimpson89](https://github.com/ssimpson89) \ No newline at end of file +* Stephen Simpson [@ssimpson89](https://github.com/ssimpson89) +* Rafael Lopez @rafalop diff --git a/LICENSE_DEPENDENCIES.md b/LICENSE_DEPENDENCIES.md index 39fa22e1..1452546c 100644 --- a/LICENSE_DEPENDENCIES.md +++ b/LICENSE_DEPENDENCIES.md @@ -317,6 +317,12 @@ The dependencies and their licenses are as follows: **License URL:** +## github.com/swaggest/swgui + +**License:** Apache-2.0 + +**License URL:** + ## github.com/vbatts/go-mtree/pkg/govis **License:** Apache-2.0 @@ -371,6 +377,12 @@ The dependencies and their licenses are as follows: **License URL:** +## gopkg.in/yaml.v2 + +**License:** Apache-2.0 + +**License URL:** + ## github.com/moby/sys/capability **License:** BSD-2-Clause @@ -467,6 +479,12 @@ The dependencies and their licenses are as follows: **License URL:** +## github.com/santhosh-tekuri/jsonschema/v3 + +**License:** BSD-3-Clause + +**License URL:** + ## github.com/spf13/pflag **License:** BSD-3-Clause @@ -629,6 +647,12 @@ The dependencies and their licenses are as follows: **License URL:** +## github.com/go-chi/chi/v5 + +**License:** MIT + +**License URL:** + ## github.com/huandu/xstrings **License:** MIT @@ -749,6 +773,42 @@ The dependencies and their licenses are as follows: **License URL:** +## github.com/swaggest/form/v5 + +**License:** MIT + +**License URL:** + +## github.com/swaggest/jsonschema-go + +**License:** MIT + +**License URL:** + +## github.com/swaggest/openapi-go + +**License:** MIT + +**License URL:** + +## github.com/swaggest/refl + +**License:** MIT + +**License URL:** + +## github.com/swaggest/rest + +**License:** MIT + +**License URL:** + +## github.com/swaggest/usecase + +**License:** MIT + +**License URL:** + ## github.com/titanous/rocacheck **License:** MIT @@ -761,6 +821,12 @@ The dependencies and their licenses are as follows: **License URL:** +## github.com/vearutop/statigz + +**License:** MIT + +**License URL:** + ## github.com/vincent-petithory/dataurl **License:** MIT diff --git a/Makefile b/Makefile index ca1ab7fc..b8118713 100644 --- a/Makefile +++ b/Makefile @@ -20,9 +20,6 @@ include Tools.mk .PHONY: build build: wwctl wwclient etc/bash_completion.d/wwctl ## Build the Warewulf binaries -.PHONY: api -api: wwapid wwapic wwapird - .PHONY: docs docs: man_pages reference ## Build the documentation @@ -48,12 +45,6 @@ config: $(config) $(config): Defaults.mk -apiconfig = etc/wwapic.conf \ - etc/wwapid.conf \ - etc/wwapird.conf -.PHONY: apiconfig -apiconfig: $(apiconfig) - %: %.in sed -ne "$(foreach V,$(VARLIST),s,@$V@,$(strip $($V)),g;)p" $@.in >$@ @@ -63,15 +54,6 @@ wwctl: $(config) $(call godeps,cmd/wwctl/main.go) wwclient: $(config) $(call godeps,cmd/wwclient/main.go) CGO_ENABLED=0 GOOS=linux go build -mod vendor -a -ldflags "-extldflags -static" -o wwclient cmd/wwclient/main.go -wwapid: $(config) $(apiconfig) $(call godeps,internal/app/api/wwapid/wwapid.go) - go build -o ./wwapid internal/app/api/wwapid/wwapid.go - -wwapic: $(config) $(apiconfig) $(call godeps,internal/app/api/wwapic/wwapic.go) - go build -o ./wwapic internal/app/api/wwapic/wwapic.go - -wwapird: $(config) $(apiconfig) $(call godeps,internal/app/api/wwapird/wwapird.go) - go build -o ./wwapird internal/app/api/wwapird/wwapird.go - .PHONY: man_pages man_pages: wwctl $(wildcard docs/man/man5/*.5) mkdir -p docs/man/man1 @@ -162,7 +144,6 @@ cleanmake: .PHONY: cleanbin cleanbin: - rm -f wwapi{c,d,rd} rm -f wwclient rm -f wwctl rm -f update_configuration @@ -207,6 +188,7 @@ install: build docs ## Install Warewulf from source # wwctl genconfig to get the compiled in paths to warewulf.conf install -d -m 0755 $(DESTDIR)$(DATADIR)/warewulf/bmc test -f $(DESTDIR)$(WWCONFIGDIR)/warewulf.conf || ./wwctl --warewulfconf etc/warewulf.conf genconfig warewulfconf print> $(DESTDIR)$(WWCONFIGDIR)/warewulf.conf + test -f $(DESTDIR)$(WWCONFIGDIR)/auth.conf || install -m 0600 etc/auth.conf $(DESTDIR)$(WWCONFIGDIR) test -f $(DESTDIR)$(WWCONFIGDIR)/nodes.conf || install -m 0644 etc/nodes.conf $(DESTDIR)$(WWCONFIGDIR) for f in etc/examples/*.ww; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/examples/; done for f in etc/ipxe/*.ipxe; do install -m 0644 $$f $(DESTDIR)$(WWCONFIGDIR)/ipxe/; done @@ -234,16 +216,10 @@ install: build docs ## Install Warewulf from source for f in docs/man/man5/*.5.gz; do install -m 0644 $$f $(DESTDIR)$(MANDIR)/man5/; done install -pd -m 0755 $(DESTDIR)$(DRACUTMODDIR)/90wwinit install -m 0644 dracut/modules.d/90wwinit/*.sh $(DESTDIR)$(DRACUTMODDIR)/90wwinit - install -D -m 0644 include/sos/warewulf.py $(DESTDIR)$(SOSPLUGINS)/warewulf.py -.PHONY: installapi -installapi: - install -m 0755 wwapic $(DESTDIR)$(BINDIR) - install -m 0755 wwapid $(DESTDIR)$(BINDIR) - install -m 0755 wwapird $(DESTDIR)$(BINDIR) - test -f $(DESTDIR)$(WWCONFIGDIR)/wwapic.conf || install -m 0644 etc/wwapic.conf $(DESTDIR)$(WWCONFIGDIR) - test -f $(DESTDIR)$(WWCONFIGDIR)/wwapid.conf || install -m 0644 etc/wwapid.conf $(DESTDIR)$(WWCONFIGDIR) - test -f $(DESTDIR)$(WWCONFIGDIR)/wwapird.conf || install -m 0644 etc/wwapird.conf $(DESTDIR)$(WWCONFIGDIR) +.PHONY: install-sos +install-sos: + install -D -m 0644 include/sos/warewulf.py $(DESTDIR)$(SOSPLUGINS)/warewulf.py .PHONY: init init: @@ -255,9 +231,6 @@ ifndef OFFLINE_BUILD wwctl: vendor wwclient: vendor update_configuration: vendor -wwapid: vendor -wwapic: vendor -wwapird: vendor dist: vendor lint: $(GOLANGCI_LINT) diff --git a/RELEASE.md b/RELEASE.md index 2ace0efc..fdd41624 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,53 +1,56 @@ # Release procedure -Major versions (e.g., v4.5.0) are tagged directly on the main -branch. Minor versions (e.g., v4.5.3) are tagged on a separate release -branch. +Major versions (e.g., v4.6.0) are tagged directly on the main branch. Minor +versions (e.g., v4.6.1) are tagged on a separate release branch. 1. Update `CHANGELOG.md`. - Identify the release date by changing "unreleased" to a date with `%Y-%m-%d` format, following existing convention in the log. - - Do any final clean-up. (e.g., remove redundancy, add issue - or PR numbers, add missing changes) + - Do any final clean-up. (e.g., remove redundancy, add issue or PR numbers, + add missing changes) - git log --graph --oneline --decorate origin/main origin/v4.5.x + git log --graph --oneline --decorate origin/main origin/v4.6.x -2. Update `userdocs` references that imply the latest release to refer - to the new version. +2. Update `userdocs` references that imply the latest release to refer to the + new version. - `contents/installation.rst` - `contributing/development-environment-vagrant.rst` - `quickstart/debian12.rst` - `quickstart/el.rst` -3. Cherry-pick updates from 1 and 2 above to a release branch if necessary. +3. Add full release notes to `userdocs/release/` and update + `userdocs/index.rst`. + +4. Add summarized release notes to `.github/releases/`. + +5. Cherry-pick updates from 1 and 2 above to a release branch if necessary. (i.e., when not doing a new major release) git cherry-pick -x -m1 --signoff -4. Create a signed tag for the release of the format v4.MINOR.PATCH, - following the format specified in . +6. Create a signed tag for the release of the format v4.MINOR.PATCH, following + the format specified in . - git tag --sign v4.5.3; git push origin v4.5.3 + git tag --sign v4.6.1; git push origin v4.6.1 -5. Monitor the release action associated with the pushed tag at - https://github.com/warewulf/warewulf/actions, and verify the - generated draft release contains the expected artifacts. This - includes the source tarball and RPMs for Suse and Rocky Linux. +7. Monitor the release action associated with the pushed tag at + https://github.com/warewulf/warewulf/actions, and verify the generated draft + release contains the expected artifacts. This includes the source tarball and + RPMs for Suse and Rocky Linux. -6. Update the release notes for the release, summarizing and expanding - on the relevant contents from . +8. Update the release notes for the release with the summary in + `.github/releases/`. -7. Confirm the correct values for the pre-release and latest release - flags. +9. Confirm the correct values for the pre-release and latest release flags. -8. Publish the release. +10. Publish the release. -9. Announce the release as a [post][1] to [warewulf.org/news][2], linking - to the GitHub release. +11. Announce the release as a [post][1] to [warewulf.org/news][2], linking to + the GitHub release. -10. Announce the release on the Warewulf Slack, linking to the +12. Announce the release on the Warewulf Slack, linking to the [warewulf.org/news][2] post. [1]: https://github.com/warewulf/warewulf.org/tree/main/src/posts diff --git a/Variables.mk b/Variables.mk index d4b04e14..4d3cb699 100644 --- a/Variables.mk +++ b/Variables.mk @@ -62,7 +62,7 @@ endif TFTPDIR ?= /var/lib/tftpboot # Warewulf directory paths -VARLIST += WWCLIENTDIR WWCONFIGDIR WWPROVISIONDIR WWOVERLAYDIR WWCHROOTDIR WWTFTPDIR WWDOCDIR IPXESOURCE +VARLIST += WWCLIENTDIR WWCONFIGDIR WWPROVISIONDIR WWOVERLAYDIR WWCHROOTDIR WWTFTPDIR WWDOCDIR IPXESOURCE SOSPLUGINS WWCONFIGDIR ?= $(SYSCONFDIR)/$(WAREWULF) WWPROVISIONDIR ?= $(LOCALSTATEDIR)/$(WAREWULF)/provision WWOVERLAYDIR ?= $(LOCALSTATEDIR)/$(WAREWULF)/overlays diff --git a/etc/auth.conf b/etc/auth.conf new file mode 100644 index 00000000..879eee8a --- /dev/null +++ b/etc/auth.conf @@ -0,0 +1,3 @@ +users: +- name: admin + password hash: "" # generate a password hash with `mkpasswd --method=bcrypt` diff --git a/etc/grub/grub.cfg.ww b/etc/grub/grub.cfg.ww index c3ad0727..9c3413a1 100644 --- a/etc/grub/grub.cfg.ww +++ b/etc/grub/grub.cfg.ww @@ -58,6 +58,8 @@ menuentry "Single-stage boot" --id single-stage { echo "!! Unable to load kernel." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Downloading images..." @@ -71,6 +73,8 @@ menuentry "Single-stage boot" --id single-stage { echo "!! Unable to load images." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Booting..." @@ -101,6 +105,8 @@ menuentry "Single-stage boot (no compression)" --id single-stage-nocompress { echo "!! Unable to load kernel." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Downloading images..." @@ -114,6 +120,8 @@ menuentry "Single-stage boot (no compression)" --id single-stage-nocompress { echo "!! Unable to load images." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Booting..." @@ -151,6 +159,8 @@ menuentry "Two stage boot with dracut" --id dracut { echo "!! Unable to load kernel." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Downloading initramfs..." @@ -161,6 +171,8 @@ menuentry "Two stage boot with dracut" --id dracut { echo "!! Unable to load initramfs." echo "!! Rebooting in 15s..." echo "!!" + sleep 15 + reboot fi echo "Booting..." diff --git a/etc/nodes.conf b/etc/nodes.conf index c5b9487f..94eeb9ee 100644 --- a/etc/nodes.conf +++ b/etc/nodes.conf @@ -23,6 +23,7 @@ nodeprofiles: args: - quiet - crashkernel=no + - net.ifnames=1 init: /sbin/init root: initramfs ipxe template: default diff --git a/go.mod b/go.mod index d6798b5a..a7f17050 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/coreos/vcontext v0.0.0-20230201181013-d72178a18687 github.com/creasty/defaults v1.8.0 github.com/fatih/color v1.18.0 + github.com/go-chi/chi/v5 v5.2.1 github.com/golang/glog v1.2.4 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 @@ -27,7 +28,12 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 + github.com/swaggest/openapi-go v0.2.55 + github.com/swaggest/rest v0.2.73 + github.com/swaggest/swgui v1.8.2 + github.com/swaggest/usecase v1.3.1 github.com/talos-systems/go-smbios v0.1.1 + golang.org/x/crypto v0.32.0 golang.org/x/sys v0.29.0 golang.org/x/term v0.28.0 google.golang.org/genproto/googleapis/api v0.0.0-20250204164813-702378808489 @@ -115,6 +121,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/rootless-containers/proto v0.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 // indirect github.com/secure-systems-lab/go-securesystemslib v0.8.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sigstore/fulcio v1.4.5 // indirect @@ -124,12 +131,16 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/stefanberger/go-pkcs11uri v0.0.0-20230803200340-78284954bff6 // indirect + github.com/swaggest/form/v5 v5.1.1 // indirect + github.com/swaggest/jsonschema-go v0.3.73 // indirect + github.com/swaggest/refl v1.3.0 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/urfave/cli v1.22.16 // indirect github.com/vbatts/go-mtree v0.5.0 // indirect github.com/vbatts/tar-split v0.11.7 // indirect github.com/vbauerster/mpb/v8 v8.7.5 // indirect + github.com/vearutop/statigz v1.4.0 // indirect github.com/vincent-petithory/dataurl v1.0.0 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect @@ -137,10 +148,10 @@ require ( go.opentelemetry.io/otel v1.32.0 // indirect go.opentelemetry.io/otel/metric v1.32.0 // indirect go.opentelemetry.io/otel/trace v1.32.0 // indirect - golang.org/x/crypto v0.32.0 // indirect golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.33.0 // indirect golang.org/x/sync v0.11.0 // indirect golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250204164813-702378808489 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e048c85a..7e7fc110 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1o github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apex/log v1.4.0 h1:jYWeNt9kWJOf1ifht8UjsCQ00eiPnFrUzCBCiiJMw/g= github.com/apex/log v1.4.0/go.mod h1:UMNC4vQNC7hb5gyr47r18ylK1n34rV7GO+gb0wpXvcE= github.com/apex/logs v0.0.7/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo= @@ -31,6 +33,11 @@ github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.38 h1:C5H9wkx/BhTYRfV14X90iIQKpSuhzsG+OHQvWdQ5YQ4= +github.com/bool64/dev v0.2.38/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= +github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -117,6 +124,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= +github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= @@ -188,6 +197,8 @@ github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09 github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -315,9 +326,13 @@ github.com/rootless-containers/proto v0.1.0/go.mod h1:vgkUFZbQd0gcE/K/ZwtE4MYjZP github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/santhosh-tekuri/jsonschema/v3 v3.1.0 h1:levPcBfnazlA1CyCMC3asL/QLZkq9pa8tQZOH513zQw= +github.com/santhosh-tekuri/jsonschema/v3 v3.1.0/go.mod h1:8kzK2TC0k0YjOForaAHdNEa7ik0fokNa2k30BKJ/W7Y= github.com/secure-systems-lab/go-securesystemslib v0.8.0 h1:mr5An6X45Kb2nddcFlbmfHkLguCE9laoZCUzEEpIZXA= github.com/secure-systems-lab/go-securesystemslib v0.8.0/go.mod h1:UH2VZVuJfCYR8WgMlCU1uFsOUU+KeyrTWcSS73NBOzU= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= @@ -358,6 +373,22 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= +github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY= +github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg= +github.com/swaggest/jsonschema-go v0.3.73 h1:gU1pBzF3pkZ1GDD3dRMdQoCjrA0sldJ+QcM7aSSPgvc= +github.com/swaggest/jsonschema-go v0.3.73/go.mod h1:qp+Ym2DIXHlHzch3HKz50gPf2wJhKOrAB/VYqLS2oJU= +github.com/swaggest/openapi-go v0.2.55 h1:PI9r7E8l0iKHriqQ6QxLbTjkZyUIHA8rlpzatYZff3c= +github.com/swaggest/openapi-go v0.2.55/go.mod h1:sTmhR2sTvauaRX45WdaqRyJVTfRQ5FxD4OPVym49BXM= +github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I= +github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg= +github.com/swaggest/rest v0.2.73 h1:CFippm6JOx9OknBSm9VVSWFrDQq98RV/11a73GJTwxM= +github.com/swaggest/rest v0.2.73/go.mod h1:oXW3+1intYTcTxrZH5snFgme+mud4Qy6iTFY34pqATg= +github.com/swaggest/swgui v1.8.2 h1:JGpRCLGLZ7EqTwHsBEOo//kx8CM7Rv3RchgvfNpB+6E= +github.com/swaggest/swgui v1.8.2/go.mod h1:nkzGeyMfq5FstGGNJKr1LORvM4RdsjTmvWvqvyZeDDc= +github.com/swaggest/usecase v1.3.1 h1:JdKV30MTSsDxAXxkldLNcEn8O2uf565khyo6gr5sS+w= +github.com/swaggest/usecase v1.3.1/go.mod h1:cae3lDd5VDmM36OQcOOOdAlEDg40TiQYIp99S9ejWqA= github.com/talos-systems/go-smbios v0.1.1 h1:Au6obB/Pp0i0JHhvPlzONk5aoNseosO2BUsmvWWi7y8= github.com/talos-systems/go-smbios v0.1.1/go.mod h1:vk76naUSZaWE8Z95wbDn51FgH0goECM4oK3KY2hYSMU= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= @@ -379,6 +410,8 @@ github.com/vbatts/tar-split v0.11.7 h1:ixZ93pO/GmvaZw4Vq9OwmfZK/kc2zKdPfu0B+gYqs github.com/vbatts/tar-split v0.11.7/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= github.com/vbauerster/mpb/v8 v8.7.5 h1:hUF3zaNsuaBBwzEFoCvfuX3cpesQXZC0Phm/JcHZQ+c= github.com/vbauerster/mpb/v8 v8.7.5/go.mod h1:bRCnR7K+mj5WXKsy0NWB6Or+wctYGvVwKn6huwvxKa0= +github.com/vearutop/statigz v1.4.0 h1:RQL0KG3j/uyA/PFpHeZ/L6l2ta920/MxlOAIGEOuwmU= +github.com/vearutop/statigz v1.4.0/go.mod h1:LYTolBLiz9oJISwiVKnOQoIwhO1LWX1A7OECawGS8XE= github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= @@ -387,6 +420,10 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= @@ -521,6 +558,7 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/app/wwclient/root.go b/internal/app/wwclient/root.go index 4530b754..a8c5afd5 100644 --- a/internal/app/wwclient/root.go +++ b/internal/app/wwclient/root.go @@ -31,6 +31,7 @@ var ( Long: "wwclient fetches the runtime overlay and puts it on the disk", RunE: CobraRunE, SilenceUsage: true, + Args: cobra.NoArgs, } DebugFlag bool PIDFile string @@ -193,8 +194,12 @@ func CobraRunE(cmd *cobra.Command, args []string) (err error) { } }() var finishedInitialSync bool = false + ipaddr := os.Getenv("WW_IPADDR") + if ipaddr == "" { + ipaddr = conf.Ipaddr + } for { - updateSystem(conf.Ipaddr, conf.Warewulf.Port, wwid, tag, localUUID) + updateSystem(ipaddr, conf.Warewulf.Port, wwid, tag, localUUID) if !finishedInitialSync { // ignore error and status here, as this wouldn't change anything _, _ = daemon.SdNotify(false, daemon.SdNotifyReady) diff --git a/internal/app/wwctl/configure/root.go b/internal/app/wwctl/configure/root.go index 40a84e02..3abd0776 100644 --- a/internal/app/wwctl/configure/root.go +++ b/internal/app/wwctl/configure/root.go @@ -17,6 +17,7 @@ var ( Long: "This application allows you to manage and initialize Warewulf dependent system\n" + "services based on the configuration in the warewulf.conf file.", RunE: CobraRunE, + Args: cobra.NoArgs, } allFunctions bool ) diff --git a/internal/app/wwctl/image/delete/root.go b/internal/app/wwctl/image/delete/root.go index 7fd1efb3..20ad8dcd 100644 --- a/internal/app/wwctl/image/delete/root.go +++ b/internal/app/wwctl/image/delete/root.go @@ -13,6 +13,7 @@ var ( Short: "Delete an imported image", Long: "This command will delete IMAGEs that have been imported into Warewulf.", RunE: CobraRunE, + Args: cobra.ArbitraryArgs, ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { if len(args) != 0 { return nil, cobra.ShellCompDirectiveNoFileComp diff --git a/internal/app/wwctl/image/list/root.go b/internal/app/wwctl/image/list/root.go index 6e5c5aea..57504b30 100644 --- a/internal/app/wwctl/image/list/root.go +++ b/internal/app/wwctl/image/list/root.go @@ -23,6 +23,7 @@ func GetCommand() *cobra.Command { Long: "This command will show you the images that are imported into Warewulf.", RunE: CobraRunE(&vars), Aliases: []string{"ls"}, + Args: cobra.ArbitraryArgs, ValidArgsFunction: completions.Images, } baseCmd.PersistentFlags().BoolVarP(&vars.full, "long", "l", false, "show all") diff --git a/internal/app/wwctl/image/rename/main_test.go b/internal/app/wwctl/image/rename/main_test.go index fe6ce724..b1e4a5cd 100644 --- a/internal/app/wwctl/image/rename/main_test.go +++ b/internal/app/wwctl/image/rename/main_test.go @@ -16,6 +16,13 @@ import ( func Test_Rename(t *testing.T) { env := testenv.New(t) env.WriteFile(path.Join(testenv.WWChrootdir, "test-image/rootfs/file"), `test`) + env.WriteFile("etc/warewulf/nodes.conf", ` +nodeprofiles: + default: + image name: test-image +nodes: + n1: + image name: test-image`) defer env.RemoveAll() warewulfd.SetNoDaemon() @@ -38,6 +45,14 @@ func Test_Rename(t *testing.T) { t.Run("Image list", func(t *testing.T) { verifyImageListOutput(t, "test-image-rename") }) + + assert.YAMLEq(t, ` +nodeprofiles: + default: + image name: test-image-rename +nodes: + n1: + image name: test-image-rename`, env.ReadFile("etc/warewulf/nodes.conf")) } func verifyImageListOutput(t *testing.T, content string) { diff --git a/internal/app/wwctl/image/root.go b/internal/app/wwctl/image/root.go index f8eefe33..f26fecf9 100644 --- a/internal/app/wwctl/image/root.go +++ b/internal/app/wwctl/image/root.go @@ -23,6 +23,7 @@ var baseCmd = &cobra.Command{ "node images. These commands will help you import, manage, and transform\n" + "images into bootable Warewulf images.", Aliases: []string{"vnfs", "container"}, + Args: cobra.NoArgs, } func init() { diff --git a/internal/app/wwctl/node/edit/root.go b/internal/app/wwctl/node/edit/root.go index c095b6b7..f80cae69 100644 --- a/internal/app/wwctl/node/edit/root.go +++ b/internal/app/wwctl/node/edit/root.go @@ -13,6 +13,7 @@ var ( Long: "This command opens an editor for the given nodes.", RunE: CobraRunE, ValidArgsFunction: completions.Nodes, + Args: cobra.ArbitraryArgs, } NoHeader bool ) diff --git a/internal/app/wwctl/node/export/root.go b/internal/app/wwctl/node/export/root.go index c3cccd35..bc62d306 100644 --- a/internal/app/wwctl/node/export/root.go +++ b/internal/app/wwctl/node/export/root.go @@ -13,6 +13,7 @@ var ( Long: "This command exports the given nodes as yaml to stdout.", RunE: CobraRunE, ValidArgsFunction: completions.Nodes, + Args: cobra.ArbitraryArgs, } NoHeader bool ) diff --git a/internal/app/wwctl/node/imprt/main.go b/internal/app/wwctl/node/imprt/main.go index f83fabd1..61e5b9cc 100644 --- a/internal/app/wwctl/node/imprt/main.go +++ b/internal/app/wwctl/node/imprt/main.go @@ -27,11 +27,10 @@ func CobraRunE(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("could not read: %s", err) } - if !ImportCVS { + if !ImportCSV { err = yaml.Unmarshal(buffer, importMap) if err == nil { - yes := util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes", len(importMap))) - if yes { + if setYes || util.Confirm(fmt.Sprintf("Are you sure you want to modify %d nodes", len(importMap))) { err = apinode.NodeAddFromYaml(&wwapiv1.NodeYaml{NodeConfMapYaml: string(buffer)}) if err != nil { return fmt.Errorf("got following problem when writing back yaml: %s", err) @@ -68,8 +67,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error { } } } - yes := util.Confirm(fmt.Sprintf("Are you sure you want to import %d nodes", len(importMap))) - if yes { + if setYes || util.Confirm(fmt.Sprintf("Are you sure you want to import %d nodes", len(importMap))) { // create second buffer an marshall nodeMap to it buffer, err = yaml.Marshal(importMap) if err != nil { diff --git a/internal/app/wwctl/node/imprt/main_test.go b/internal/app/wwctl/node/imprt/main_test.go new file mode 100644 index 00000000..8ac0ceca --- /dev/null +++ b/internal/app/wwctl/node/imprt/main_test.go @@ -0,0 +1,81 @@ +package imprt + +import ( + "bytes" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" +) + +func Test_Node_Import(t *testing.T) { + tests := map[string]struct { + args []string + importFile string + wantErr bool + inDB string + outDB string + }{ + "import new node": { + args: []string{"importFile"}, + importFile: ` +n1: + id: n1 + profiles: + - default + network devices: + eth0: + device: eth0 + hwaddr: c4:cb:e1:bb:dd:e9 + ipaddr: 192.168.1.10`, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: {}`, + outDB: ` +nodeprofiles: {} +nodes: + n1: + profiles: + - default + network devices: + eth0: + device: eth0 + hwaddr: c4:cb:e1:bb:dd:e9 + ipaddr: 192.168.1.10`, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll() + { + wd, err := os.Getwd() + assert.NoError(t, err) + defer func() { assert.NoError(t, os.Chdir(wd)) }() + } + assert.NoError(t, os.Chdir(env.GetPath("."))) + env.WriteFile("./importFile", tt.importFile) + env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) + warewulfd.SetNoDaemon() + + baseCmd := GetCommand() + args := append(tt.args, "--yes") + baseCmd.SetArgs(args) + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + err := baseCmd.Execute() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + content := env.ReadFile("etc/warewulf/nodes.conf") + assert.YAMLEq(t, tt.outDB, content) + } + }) + } +} diff --git a/internal/app/wwctl/node/imprt/root.go b/internal/app/wwctl/node/imprt/root.go index 3aa9bbcc..225b0e64 100644 --- a/internal/app/wwctl/node/imprt/root.go +++ b/internal/app/wwctl/node/imprt/root.go @@ -9,18 +9,25 @@ import ( var ( baseCmd = &cobra.Command{ DisableFlagsInUseLine: true, - Use: "import [OPTIONS] NODENAME", - Short: "Import node(s) from yaml file", + Use: "import [OPTIONS] FILE", + Short: "Import node(s) from yaml FILE", Long: "This command imports all the nodes defined in a file. It will overwrite nodes with same name.", RunE: CobraRunE, Args: cobra.ExactArgs(1), Aliases: []string{"import"}, } - ImportCVS bool + ImportCSV bool + setYes bool ) func init() { - baseCmd.PersistentFlags().BoolVarP(&ImportCVS, "cvs", "c", false, "Import CVS file") + baseCmd.PersistentFlags().BoolVarP(&ImportCSV, "csv", "c", false, "Import CSV file") + baseCmd.Flags().BoolVar(&ImportCSV, "cvs", false, "Import CSV file") + baseCmd.Flags().Lookup("cvs").Hidden = true + if err := baseCmd.Flags().MarkDeprecated("cvs", "use --csv instead"); err != nil { + panic(err) + } + baseCmd.PersistentFlags().BoolVarP(&setYes, "yes", "y", false, "Set 'yes' to all questions asked") } // GetRootCommand returns the root cobra.Command for the application. diff --git a/internal/app/wwctl/node/list/main_test.go b/internal/app/wwctl/node/list/main_test.go index 013cea36..808eb3e5 100644 --- a/internal/app/wwctl/node/list/main_test.go +++ b/internal/app/wwctl/node/list/main_test.go @@ -187,6 +187,18 @@ nodes: profiles: - default `, + }, + { + name: "node list with no ipmi data", + args: []string{"--ipmi"}, + wantErr: false, + stdout: ` +NODE IPMI IPADDR IPMI PORT IPMI USERNAME IPMI INTERFACE +---- ----------- --------- ------------- -------------- +n1 -- -- -- --`, + inDb: ` +nodes: + n1: {}`, }, { name: "node list profile with ipmi user", @@ -447,27 +459,9 @@ nodes: stdout: ` { "n01": { - "Discoverable": "", - "AssetKey": "", - "Profiles": [ + "profiles": [ "default" - ], - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null + ] } } `, @@ -486,50 +480,14 @@ nodes: stdout: ` { "n01": { - "Discoverable": "", - "AssetKey": "", - "Profiles": [ + "profiles": [ "default" - ], - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null + ] }, "n02": { - "Discoverable": "", - "AssetKey": "", - "Profiles": [ + "profiles": [ "default" - ], - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null + ] } } `, diff --git a/internal/app/wwctl/node/list/root.go b/internal/app/wwctl/node/list/root.go index bf8805c4..3d8e35c8 100644 --- a/internal/app/wwctl/node/list/root.go +++ b/internal/app/wwctl/node/list/root.go @@ -25,6 +25,7 @@ func GetCommand() *cobra.Command { RunE: CobraRunE(&vars), Aliases: []string{"ls"}, ValidArgsFunction: completions.Nodes, + Args: cobra.ArbitraryArgs, } baseCmd.PersistentFlags().BoolVarP(&vars.showNet, "net", "n", false, "Show node network configurations") baseCmd.PersistentFlags().BoolVarP(&vars.showIpmi, "ipmi", "i", false, "Show node IPMI configurations") diff --git a/internal/app/wwctl/node/root.go b/internal/app/wwctl/node/root.go index 062f630b..c71e1451 100644 --- a/internal/app/wwctl/node/root.go +++ b/internal/app/wwctl/node/root.go @@ -23,6 +23,7 @@ var ( "node ranges. For example: n00[00-4].cluster[0-1] will identify the first 5 nodes\n" + "in cluster0 and cluster1.", Aliases: []string{"nodes"}, + Args: cobra.NoArgs, } ) diff --git a/internal/app/wwctl/node/set/main_test.go b/internal/app/wwctl/node/set/main_test.go index 5ae8cb91..f3c5ea3d 100644 --- a/internal/app/wwctl/node/set/main_test.go +++ b/internal/app/wwctl/node/set/main_test.go @@ -9,179 +9,120 @@ import ( "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -type test_description struct { - name string - args []string - wantErr bool - stdout string - inDB string - outDb string -} - -func run_test(t *testing.T, test test_description) { - env := testenv.New(t) - defer env.RemoveAll() - env.WriteFile("etc/warewulf/nodes.conf", test.inDB) - warewulfd.SetNoDaemon() - name := test.name - if name == "" { - name = t.Name() - } - t.Run(name, func(t *testing.T) { - baseCmd := GetCommand() - test.args = append(test.args, "--yes") - baseCmd.SetArgs(test.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - err := baseCmd.Execute() - if test.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, buf.String(), test.stdout) - content := env.ReadFile("etc/warewulf/nodes.conf") - assert.YAMLEq(t, test.outDb, content) - } - }) -} - -func Test_Single_Node_Change_Profile(t *testing.T) { - test := test_description{ - args: []string{"--profile=foo", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: +func Test_Node_Set(t *testing.T) { + tests := map[string]struct { + args []string + wantErr bool + inDB string + outDB string + }{ + "--profile=foo": { + args: []string{"--profile=foo", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - default`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - - foo -`, - } - run_test(t, test) -} - -func Test_Node_Unset(t *testing.T) { - test := test_description{ - args: []string{"--comment=UNDEF", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + - foo`, + }, + "--comment=UNDEF": { + args: []string{"--comment=UNDEF", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: n01: comment: foo profiles: - default`, - outDb: `nodeprofiles: {} + outDB: ` +nodeprofiles: {} nodes: n01: profiles: - - default -`, - } - run_test(t, test) -} - -func Test_Set_Ipmi_Write_Explicit(t *testing.T) { - test := test_description{ - args: []string{"--ipmiwrite", "true", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + - default`, + }, + "--ipmiwrite=true": { + args: []string{"--ipmiwrite", "true", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: - n01: {} -`, - outDb: `nodeprofiles: {} + n01: {}`, + outDB: ` +nodeprofiles: {} nodes: n01: ipmi: - write: "true" -`, - } - run_test(t, test) -} - -func Test_Set_Ipmi_Write_Implicit(t *testing.T) { - test := test_description{ - args: []string{"--ipmiwrite", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + write: "true"`, + }, + "--ipmiwrite": { + args: []string{"--ipmiwrite", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: - n01: {} -`, - outDb: `nodeprofiles: {} + n01: {}`, + outDB: ` +nodeprofiles: {} nodes: n01: ipmi: - write: "true" -`, - } - run_test(t, test) -} - -func Test_Unset_Ipmi_Write(t *testing.T) { - test := test_description{ - args: []string{"--ipmiwrite=UNDEF", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + write: "true"`, + }, + "--ipmiwrite=UNDEF": { + args: []string{"--ipmiwrite=UNDEF", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: n01: ipmi: - write: "true" -`, - outDb: `nodeprofiles: {} + write: "true"`, + outDB: ` +nodeprofiles: {} nodes: - n01: {} -`, - } - run_test(t, test) -} - -func Test_Unset_Ipmi_Write_False(t *testing.T) { - test := test_description{ - args: []string{"--ipmiwrite=UNDEF", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + n01: {}`, + }, + "--ipmiwrite=false": { + args: []string{"--ipmiwrite=false", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: + n01: {}`, + outDB: ` +nodeprofiles: {} nodes: n01: ipmi: - write: "false" -`, - outDb: `nodeprofiles: {} -nodes: - n01: {} -`, - } - run_test(t, test) -} - -func Test_Ipmi_Hidden_False(t *testing.T) { - test := test_description{ - args: []string{"--ipmiwrite=false", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: + write: "false"`, + }, + "--ipmiwrite=false (override)": { + args: []string{"--ipmiwrite=false", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: default: ipmi: write: "true" nodes: n01: profiles: - - default -`, - outDb: `nodeprofiles: + - default`, + outDB: ` +nodeprofiles: default: ipmi: write: "true" @@ -190,84 +131,66 @@ nodes: profiles: - default ipmi: - write: "false" -`, - } - run_test(t, test) -} - -func Test_Add_NetTags(t *testing.T) { - test := test_description{ - args: []string{"--nettagadd=dns=1.1.1.1", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + write: "false"`, + }, + "--nettagadd": { + args: []string{"--nettagadd=dns=1.1.1.1", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: - n01: {} -`, - outDb: `nodeprofiles: {} + n01: {}`, + outDB: ` +nodeprofiles: {} nodes: n01: network devices: default: tags: - dns: 1.1.1.1 -`, - } - run_test(t, test) -} - -func Test_Del_NetTags(t *testing.T) { - test := test_description{ - args: []string{"--netname=default", "--nettagdel=dns1,dns2", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: {} + dns: 1.1.1.1`, + }, + "--nettagdel": { + args: []string{"--nettagdel=dns1,dns2", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: {} nodes: n01: network devices: default: tags: dns1: 1.1.1.1 - dns2: 2.2.2.2 -`, - outDb: `nodeprofiles: {} + dns2: 2.2.2.2`, + outDB: ` +nodeprofiles: {} nodes: - n01: {} -`, - } - run_test(t, test) -} - -func Test_Multiple_Set_Tests(t *testing.T) { - tests := []test_description{ - { - name: "single node change profile", + n01: {}`, + }, + "single node change profile": { args: []string{"--profile=foo", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - default`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - - foo -`, + - foo`, }, - { - name: "multiple nodes change profile", + "multiple nodes change profile": { args: []string{"--profile=foo", "n0[1-2]"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -277,7 +200,8 @@ nodes: n02: profiles: - default`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -286,22 +210,21 @@ nodes: - foo n02: profiles: - - foo -`, + - foo`, }, - { - name: "single node set ipmitag", + "single node set ipmitag": { args: []string{"--ipmitagadd", "foo=baar", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - default`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -310,15 +233,13 @@ nodes: - default ipmi: tags: - foo: baar -`, + foo: baar`, }, - { - name: "single node delete tag", + "single node delete tag": { args: []string{"--tagdel", "tag1", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -328,7 +249,8 @@ nodes: tags: tag1: value1 tag2: value2`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -336,42 +258,37 @@ nodes: profiles: - default tags: - tag2: value2 -`, + tag2: value2`, }, - { - name: "single node add tag", + "single node add tag": { args: []string{"--tagadd", "tag1=foobaar", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: {} nodes: - n01: {} -`, - outDb: `nodeprofiles: + n01: {}`, + outDB: ` +nodeprofiles: default: {} nodes: n01: tags: - tag1: foobaar -`, + tag1: foobaar`, }, - { - name: "single node add tag with netdev", + "single node add tag with netdev": { args: []string{"--tagadd", "tag1=foobaar", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: {} nodes: n01: network devices: default: - ipaddr: 172.16.130.101 - -`, - outDb: `nodeprofiles: + ipaddr: 172.16.130.101`, + outDB: ` +nodeprofiles: default: {} nodes: n01: @@ -379,48 +296,43 @@ nodes: tag1: foobaar network devices: default: - ipaddr: 172.16.130.101 -`, + ipaddr: 172.16.130.101`, }, - { - name: "single node set onboot", + "single node set onboot": { args: []string{"--netname", "default", "--onboot=true", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: + default: {} +nodes: + n01: + network devices: + default: + ipaddr: 172.16.130.101`, + outDB: ` +nodeprofiles: default: {} nodes: n01: network devices: default: ipaddr: 172.16.130.101 - -`, - outDb: `nodeprofiles: - default: {} -nodes: - n01: - network devices: - default: - ipaddr: 172.16.130.101 - onboot: "true" -`, + onboot: "true"`, }, - { - name: "single node set fs,part and disk", + "single node set fs,part and disk": { args: []string{"--fsname=var", "--fspath=/var", "--fsformat=btrfs", "--partname=var", "--partnumber=1", "--diskname=/dev/vda", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - - default -`, - outDb: `nodeprofiles: + - default`, + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -435,15 +347,13 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, + path: /var`, }, - { - name: "single delete not existing fs", + "single delete not existing fs": { args: []string{"--fsdel=foo", "n01"}, wantErr: true, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -459,9 +369,9 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, - outDb: `nodeprofiles: + path: /var`, + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -476,15 +386,13 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, + path: /var`, }, - { - name: "single node delete existing fs", + "single node delete existing fs": { args: []string{"--fsdel=/dev/disk/by-partlabel/var", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -500,9 +408,9 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, - outDb: `nodeprofiles: + path: /var`, + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -513,15 +421,13 @@ nodes: /dev/vda: partitions: var: - number: "1" -`, + number: "1"`, }, - { - name: "single node delete existing partition", + "single node delete existing partition": { args: []string{"--partdel=var", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -537,9 +443,9 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, - outDb: `nodeprofiles: + path: /var`, + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -551,15 +457,13 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, + path: /var`, }, - { - name: "single node delete existing disk", + "single node delete existing disk": { args: []string{"--diskdel=/dev/vda", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: @@ -574,9 +478,9 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, - outDb: `nodeprofiles: + path: /var`, + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -586,22 +490,21 @@ nodes: filesystems: /dev/disk/by-partlabel/var: format: btrfs - path: /var -`, + path: /var`, }, - { - name: "single node set mtu", + "single node set mtu": { args: []string{"--mtu", "1234", "--netname=mynet", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit nodes: n01: profiles: - default`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit nodes: @@ -610,15 +513,13 @@ nodes: - default network devices: mynet: - mtu: "1234" -`, + mtu: "1234"`, }, - { - name: "single node set ipmitag", + "single node set tag": { args: []string{"--tagadd", "nodetag1=nodevalue1", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: p1: comment: testit 1 tags: @@ -632,7 +533,8 @@ nodes: profiles: - p1 - p2`, - outDb: `nodeprofiles: + outDB: ` +nodeprofiles: p1: comment: testit 1 tags: @@ -647,39 +549,25 @@ nodes: - p1 - p2 tags: - nodetag1: nodevalue1 -`, + nodetag1: nodevalue1`, }, - { - name: "single node set comma in comment", + "single node set comma in comment": { args: []string{"n01", "--comment", "This is a , comment"}, wantErr: false, - stdout: "", inDB: `nodes: n01: - comment: old comment -`, - outDb: `nodeprofiles: {} + comment: old comment`, + outDB: ` +nodeprofiles: {} nodes: n01: - comment: This is a , comment -`, + comment: This is a , comment`, }, - } - - warewulfd.SetNoDaemon() - for _, tt := range tests { - run_test(t, tt) - } -} - -func Test_Node_Add(t *testing.T) { - tests := []test_description{ - { + "--tagadd (one)": { args: []string{"--tagadd=email=node", "n01"}, wantErr: false, - stdout: "", - inDB: `nodeprofiles: + inDB: ` +nodeprofiles: default: comment: testit tags: @@ -688,24 +576,8 @@ nodes: n01: profiles: - default`, - outDb: `nodeprofiles: - default: - comment: testit - tags: - email: profile -nodes: - n01: - profiles: - - default - tags: - email: node -`, - }, - { - args: []string{"--tagadd=newtag=newval", "n01"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: + outDB: ` +nodeprofiles: default: comment: testit tags: @@ -716,7 +588,24 @@ nodes: - default tags: email: node`, - outDb: `nodeprofiles: + }, + "--tagadd (second)": { + args: []string{"--tagadd=newtag=newval", "n01"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: + comment: testit + tags: + email: profile +nodes: + n01: + profiles: + - default + tags: + email: node`, + outDB: ` +nodeprofiles: default: comment: testit tags: @@ -727,12 +616,72 @@ nodes: - default tags: email: node - newtag: newval -`, + newtag: newval`, + }, + "--image=UNDEF": { + args: []string{"--image=UNDEF", "n1"}, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: + n1: + image: rockylinux-9`, + outDB: ` +nodeprofiles: {} +nodes: + n1: {}`, + }, + "--image=UNSET": { + args: []string{"--image=UNSET", "n1"}, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: + n1: + image: rockylinux-9`, + outDB: ` +nodeprofiles: {} +nodes: + n1: {}`, + }, + "--ipaddr=0.0.0.0 (unset)": { + args: []string{"--ipaddr=0.0.0.0", "n1"}, + wantErr: false, + inDB: ` +nodeprofiles: {} +nodes: + n1: + network devices: + default: + ipadddr: 192.168.0.1`, + outDB: ` +nodeprofiles: {} +nodes: + n1: {}`, }, } - for _, tt := range tests { - run_test(t, tt) + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll() + env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) + warewulfd.SetNoDaemon() + + baseCmd := GetCommand() + args := append(tt.args, "--yes") + baseCmd.SetArgs(args) + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + err := baseCmd.Execute() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + content := env.ReadFile("etc/warewulf/nodes.conf") + assert.YAMLEq(t, tt.outDB, content) + } + }) } } diff --git a/internal/app/wwctl/node/status/root.go b/internal/app/wwctl/node/status/root.go index d6cfd4d1..0ccb3dc7 100644 --- a/internal/app/wwctl/node/status/root.go +++ b/internal/app/wwctl/node/status/root.go @@ -13,6 +13,7 @@ var ( Long: "View and monitor the status of nodes as they are provisioned and check in.", RunE: CobraRunE, ValidArgsFunction: completions.Nodes, + Args: cobra.ArbitraryArgs, } SetWatch bool SetUpdate int diff --git a/internal/app/wwctl/overlay/build/root.go b/internal/app/wwctl/overlay/build/root.go index 2c4fac86..041da655 100644 --- a/internal/app/wwctl/overlay/build/root.go +++ b/internal/app/wwctl/overlay/build/root.go @@ -13,6 +13,7 @@ var ( Long: "This command builds overlays for given nodes.", RunE: CobraRunE, ValidArgsFunction: completions.Nodes, + Args: cobra.ArbitraryArgs, } OverlayNames []string OverlayDir string diff --git a/internal/app/wwctl/overlay/list/root.go b/internal/app/wwctl/overlay/list/root.go index ff8fb903..d5031e1d 100644 --- a/internal/app/wwctl/overlay/list/root.go +++ b/internal/app/wwctl/overlay/list/root.go @@ -14,6 +14,7 @@ var ( RunE: CobraRunE, Aliases: []string{"ls"}, ValidArgsFunction: completions.Overlays, + Args: cobra.ArbitraryArgs, } ListContents bool ListLong bool diff --git a/internal/app/wwctl/overlay/root.go b/internal/app/wwctl/overlay/root.go index cbf6ca67..529da93c 100644 --- a/internal/app/wwctl/overlay/root.go +++ b/internal/app/wwctl/overlay/root.go @@ -20,6 +20,7 @@ var ( Use: "overlay COMMAND [OPTIONS]", Short: "Warewulf Overlay Management", Long: "Management interface for Warewulf overlays", + Args: cobra.NoArgs, } ) diff --git a/internal/app/wwctl/power/root.go b/internal/app/wwctl/power/root.go index 180ab219..58347b70 100644 --- a/internal/app/wwctl/power/root.go +++ b/internal/app/wwctl/power/root.go @@ -16,6 +16,7 @@ var ( Use: "power COMMAND [OPTIONS]", Short: "Warewulf node power management", Long: "This command controls the power state of nodes.", + Args: cobra.NoArgs, } ) diff --git a/internal/app/wwctl/profile/add/root.go b/internal/app/wwctl/profile/add/root.go index 255bcef8..ad415aa7 100644 --- a/internal/app/wwctl/profile/add/root.go +++ b/internal/app/wwctl/profile/add/root.go @@ -24,6 +24,7 @@ func GetCommand() *cobra.Command { Aliases: []string{"new", "create"}, RunE: CobraRunE(&vars), ValidArgsFunction: completions.None, + Args: cobra.MinimumNArgs(1), } vars.profileConf.CreateFlags(baseCmd) vars.profileAdd.CreateAddFlags(baseCmd) diff --git a/internal/app/wwctl/profile/edit/root.go b/internal/app/wwctl/profile/edit/root.go index 078c9cc1..4b4c323b 100644 --- a/internal/app/wwctl/profile/edit/root.go +++ b/internal/app/wwctl/profile/edit/root.go @@ -13,6 +13,7 @@ var ( Long: "This command opens an editor for the given profiles.", RunE: CobraRunE, ValidArgsFunction: completions.Profiles, + Args: cobra.ArbitraryArgs, } NoHeader bool ) diff --git a/internal/app/wwctl/profile/list/main_test.go b/internal/app/wwctl/profile/list/main_test.go index e7b57358..4d053fd8 100644 --- a/internal/app/wwctl/profile/list/main_test.go +++ b/internal/app/wwctl/profile/list/main_test.go @@ -153,25 +153,7 @@ nodes: args: []string{"-j"}, output: ` { - "default": { - "Profiles": null, - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null - } + "default": {} } `, inDb: ` @@ -205,44 +187,8 @@ nodes: args: []string{"-j"}, output: ` { - "default": { - "Profiles": null, - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null - }, - "test": { - "Profiles": null, - "Comment": "", - "ClusterName": "", - "ImageName": "", - "Ipxe": "", - "RuntimeOverlay": null, - "SystemOverlay": null, - "Kernel": null, - "Ipmi": null, - "Init": "", - "Root": "", - "NetDevs": null, - "Tags": null, - "PrimaryNetDev": "", - "Disks": null, - "FileSystems": null, - "Resources": null - } + "default": {}, + "test": {} } `, inDb: ` diff --git a/internal/app/wwctl/profile/list/root.go b/internal/app/wwctl/profile/list/root.go index 86468943..91cc7492 100644 --- a/internal/app/wwctl/profile/list/root.go +++ b/internal/app/wwctl/profile/list/root.go @@ -22,6 +22,7 @@ func GetCommand() *cobra.Command { RunE: CobraRunE(&vars), Aliases: []string{"ls"}, ValidArgsFunction: completions.Profiles, + Args: cobra.ArbitraryArgs, } baseCmd.PersistentFlags().BoolVarP(&vars.showAll, "all", "a", false, "Show all profile configurations") baseCmd.PersistentFlags().BoolVarP(&vars.showYaml, "yaml", "y", false, "Show profile configurations via yaml format") diff --git a/internal/app/wwctl/profile/root.go b/internal/app/wwctl/profile/root.go index 4cd354ca..19092929 100644 --- a/internal/app/wwctl/profile/root.go +++ b/internal/app/wwctl/profile/root.go @@ -15,6 +15,7 @@ var ( Use: "profile COMMAND [OPTIONS]", Short: "Node configuration profile management", Long: "Management of node profile settings", + Args: cobra.NoArgs, } ) diff --git a/internal/app/wwctl/profile/set/main_test.go b/internal/app/wwctl/profile/set/main_test.go index d12abee6..d1fc15fa 100644 --- a/internal/app/wwctl/profile/set/main_test.go +++ b/internal/app/wwctl/profile/set/main_test.go @@ -5,105 +5,130 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/warewulf/warewulf/internal/pkg/testenv" "github.com/warewulf/warewulf/internal/pkg/warewulfd" ) -type test_description struct { - name string - args []string - wantErr bool - stdout string - inDB string - outDb string -} - -func run_test(t *testing.T, test test_description) { - env := testenv.New(t) - defer env.RemoveAll() - env.WriteFile("etc/warewulf/nodes.conf", test.inDB) - warewulfd.SetNoDaemon() - name := test.name - if name == "" { - name = t.Name() +func Test_Profile_Set(t *testing.T) { + tests := map[string]struct { + args []string + wantErr bool + inDB string + outDb string + }{ + "Test_Set_Netdev": { + args: []string{"--netdev=eth0", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: {} +nodes: {}`, + outDb: ` +nodeprofiles: + default: + network devices: + default: + device: eth0 +nodes: {}`, + }, + "Test_Set_Netdev_and_Mask": { + args: []string{"--netdev=eth0", "-M=255.255.255.0", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: {} +nodes: {}`, + outDb: ` +nodeprofiles: + default: + network devices: + default: + device: eth0 + netmask: 255.255.255.0 +nodes: {}`, + }, + "Set Mask Existing NetDev": { + args: []string{"-M=255.255.255.0", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: + network devices: + default: + device: eth0 +nodes: {}`, + outDb: ` +nodeprofiles: + default: + network devices: + default: + device: eth0 + netmask: 255.255.255.0 +nodes: {}`, + }, + "--image=UNSET": { + args: []string{"--image=UNSET", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: + image: rockylinux-9 +nodes: {}`, + outDb: ` +nodeprofiles: + default: {} +nodes: {}`, + }, + "--image=UNDEF": { + args: []string{"--image=UNDEF", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: + image: rockylinux-9 +nodes: {}`, + outDb: ` +nodeprofiles: + default: {} +nodes: {}`, + }, + "--tagadd=mytag=0.0.0.0": { + args: []string{"--tagadd=mytag=0.0.0.0", "default"}, + wantErr: false, + inDB: ` +nodeprofiles: + default: {} +nodes: {}`, + outDb: ` +nodeprofiles: + default: + tags: + mytag: 0.0.0.0 +nodes: {}`, + }, } - t.Run(name, func(t *testing.T) { - baseCmd := GetCommand() - test.args = append(test.args, "--yes") - baseCmd.SetArgs(test.args) - buf := new(bytes.Buffer) - baseCmd.SetOut(buf) - baseCmd.SetErr(buf) - err := baseCmd.Execute() - if test.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, buf.String(), test.stdout) - content := env.ReadFile("etc/warewulf/nodes.conf") - assert.YAMLEq(t, test.outDb, content) - } - }) -} -func Test_Set_Netdev(t *testing.T) { - test := test_description{ - args: []string{"--netname=default", "--netdev=eth0", "default"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: - default: {} -nodes: {} -`, - outDb: `nodeprofiles: - default: - network devices: - default: - device: eth0 -nodes: {} -`} - run_test(t, test) -} -func Test_Set_Netdev_and_Mask(t *testing.T) { - test := test_description{ - args: []string{"--netname=default", "--netdev=eth0", "-M=255.255.255.0", "default"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: - default: {} -nodes: {} -`, - outDb: `nodeprofiles: - default: - network devices: - default: - device: eth0 - netmask: 255.255.255.0 -nodes: {} -`} - run_test(t, test) -} + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll() + env.WriteFile("etc/warewulf/nodes.conf", tt.inDB) + warewulfd.SetNoDaemon() + baseCmd := GetCommand() + args := append(tt.args, "--yes") + baseCmd.SetArgs(args) + buf := new(bytes.Buffer) + baseCmd.SetOut(buf) + baseCmd.SetErr(buf) + err := baseCmd.Execute() + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + content := env.ReadFile("etc/warewulf/nodes.conf") + assert.YAMLEq(t, tt.outDb, content) + } -func Test_Set_Mask_Existing_NetDev(t *testing.T) { - test := test_description{ - args: []string{"--netname=default", "-M=255.255.255.0", "default"}, - wantErr: false, - stdout: "", - inDB: `nodeprofiles: - default: - network devices: - default: - device: eth0 -nodes: {} -`, - outDb: `nodeprofiles: - default: - network devices: - default: - device: eth0 - netmask: 255.255.255.0 -nodes: {} -`} - run_test(t, test) + }) + } } diff --git a/internal/app/wwctl/profile/set/root.go b/internal/app/wwctl/profile/set/root.go index df76aab6..6627ff21 100644 --- a/internal/app/wwctl/profile/set/root.go +++ b/internal/app/wwctl/profile/set/root.go @@ -35,6 +35,7 @@ func GetCommand() *cobra.Command { Aliases: []string{"modify"}, RunE: CobraRunE(&vars), ValidArgsFunction: completions.Profiles, + Args: cobra.ArbitraryArgs, } vars.profileConf.CreateFlags(baseCmd) vars.profileDel.CreateDelFlags(baseCmd) diff --git a/internal/app/wwctl/root.go b/internal/app/wwctl/root.go index 7e464333..a0e18b2a 100644 --- a/internal/app/wwctl/root.go +++ b/internal/app/wwctl/root.go @@ -30,6 +30,7 @@ var ( PersistentPreRunE: rootPersistentPreRunE, SilenceUsage: true, SilenceErrors: true, + Args: cobra.NoArgs, } verboseArg bool DebugFlag bool diff --git a/internal/app/wwctl/server/root.go b/internal/app/wwctl/server/root.go index dd3f3a00..86585c12 100644 --- a/internal/app/wwctl/server/root.go +++ b/internal/app/wwctl/server/root.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "github.com/warewulf/warewulf/internal/app/wwctl/completions" "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/warewulfd/server" ) var ( @@ -31,7 +32,7 @@ func CobraRunE(cmd *cobra.Command, args []string) error { if err := warewulfd.DaemonInitLogging(); err != nil { return fmt.Errorf("failed to configure logging: %w", err) } - if err := warewulfd.RunServer(); err != nil { + if err := server.RunServer(); err != nil { return fmt.Errorf("failed to start Warewulf server: %w", err) } return nil diff --git a/internal/pkg/api/image/image.go b/internal/pkg/api/image/image.go index be9995a0..6d3cb0da 100644 --- a/internal/pkg/api/image/image.go +++ b/internal/pkg/api/image/image.go @@ -90,18 +90,19 @@ func ImageDelete(cdp *wwapiv1.ImageDeleteParameter) (err error) { return fmt.Errorf("could not open nodeDB: %s", err) } - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return - } - ARG_LOOP: for i := 0; i < len(cdp.ImageNames); i++ { //_, arg := range args { imageName := cdp.ImageNames[i] - for _, n := range nodes { + for _, n := range nodeDB.Nodes { if n.ImageName == imageName { - wwlog.Error("image is configured for nodes, skipping: %s", imageName) + wwlog.Error("image %s is in use by node %s, skipping", imageName, n.Id()) + continue ARG_LOOP + } + } + for _, p := range nodeDB.NodeProfiles { + if p.ImageName == imageName { + wwlog.Error("image %s is in use by profile %s, skipping", imageName, p.Id()) continue ARG_LOOP } } @@ -162,7 +163,7 @@ func ImageImport(cip *wwapiv1.ImageImportParameter) (imageName string, err error } else if strings.HasPrefix(cip.Source, "docker://") || strings.HasPrefix(cip.Source, "docker-daemon://") || strings.HasPrefix(cip.Source, "file://") || util.IsFile(cip.Source) { var sCtx *types.SystemContext - sCtx, err = getSystemContext(cip.OciNoHttps, cip.OciUsername, cip.OciPassword, cip.Platform) + sCtx, err = GetSystemContext(cip.OciNoHttps, cip.OciUsername, cip.OciPassword, cip.Platform) if err != nil { return } @@ -355,23 +356,17 @@ func ImageRename(crp *wwapiv1.ImageRenameParameter) (err error) { return err } - nodes, err := nodeDB.FindAllNodes() - if err != nil { - return err - } - for _, node := range nodes { + for nodeId, node := range nodeDB.Nodes { if node.ImageName == crp.ImageName { - node.ImageName = crp.TargetName + wwlog.Debug("updating node %s image to %s", nodeId, crp.TargetName) + nodeDB.Nodes[nodeId].ImageName = crp.TargetName } } - profiles, err := nodeDB.FindAllProfiles() - if err != nil { - return err - } - for _, profile := range profiles { + for profileId, profile := range nodeDB.NodeProfiles { if profile.ImageName == crp.ImageName { - profile.ImageName = crp.TargetName + wwlog.Debug("updating profile %s image to %s", profileId, crp.TargetName) + nodeDB.NodeProfiles[profileId].ImageName = crp.TargetName } } @@ -391,7 +386,7 @@ func ImageRename(crp *wwapiv1.ImageRenameParameter) (err error) { } // create the system context and reading out environment variables -func getSystemContext(noHttps bool, username string, password string, platform string) (sCtx *types.SystemContext, err error) { +func GetSystemContext(noHttps bool, username string, password string, platform string) (sCtx *types.SystemContext, err error) { sCtx = &types.SystemContext{} // only check env if noHttps wasn't set if !noHttps { diff --git a/internal/pkg/api/node/edit.go b/internal/pkg/api/node/edit.go index 31f46358..915ace54 100644 --- a/internal/pkg/api/node/edit.go +++ b/internal/pkg/api/node/edit.go @@ -21,8 +21,14 @@ func NodeAddFromYaml(nodeList *wwapiv1.NodeYaml) (err error) { if err != nil { return fmt.Errorf("could not unmarshal Yaml: %w", err) } - for nodeName, node := range nodeMap { - err = nodeDB.SetNode(nodeName, *node) + for nodeName, nodeData := range nodeMap { + if _, err = nodeDB.GetNodeOnly(nodeName); err == node.ErrNotFound { + _, err = nodeDB.AddNode(nodeName) + if err != nil { + return fmt.Errorf("couldn't add new node: %w", err) + } + } + err = nodeDB.SetNode(nodeName, *nodeData) if err != nil { return fmt.Errorf("couldn't set node: %w", err) } diff --git a/internal/pkg/api/node/list.go b/internal/pkg/api/node/list.go index eafdabb4..abe1b2c5 100644 --- a/internal/pkg/api/node/list.go +++ b/internal/pkg/api/node/list.go @@ -64,13 +64,20 @@ func NodeList(nodeGet *wwapiv1.GetNodeList) (nodeList wwapiv1.NodeList, err erro nodeList.Output = append(nodeList.Output, fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s", "NODE", "IPMI IPADDR", "IPMI PORT", "IPMI USERNAME", "IPMI INTERFACE")) for _, n := range node.FilterNodeListByName(nodes, nodeGet.Nodes) { + ipaddr, port, username, iface := "", "", "", "" + if n.Ipmi != nil { + ipaddr = n.Ipmi.Ipaddr.String() + port = n.Ipmi.Port + username = n.Ipmi.UserName + iface = n.Ipmi.Interface + } nodeList.Output = append(nodeList.Output, fmt.Sprintf("%s:=:%s:=:%s:=:%s:=:%s", n.Id(), - n.Ipmi.Ipaddr.String(), - n.Ipmi.Port, - n.Ipmi.UserName, - n.Ipmi.Interface)) + ipaddr, + port, + username, + iface)) } } else if nodeGet.Type == wwapiv1.GetNodeList_Long { nodeList.Output = append(nodeList.Output, diff --git a/internal/pkg/config/api.go b/internal/pkg/config/api.go new file mode 100644 index 00000000..349909c4 --- /dev/null +++ b/internal/pkg/config/api.go @@ -0,0 +1,50 @@ +package config + +import ( + "net" + + "github.com/creasty/defaults" +) + +type IPNet net.IPNet + +func (n IPNet) MarshalText() ([]byte, error) { + ipnet := n.IPNet() + return []byte((&ipnet).String()), nil +} + +func (n *IPNet) UnmarshalText(text []byte) error { + _, network, err := net.ParseCIDR(string(text)) + if err != nil { + return err + } + *n = IPNet(*network) + return nil +} + +func (n IPNet) IPNet() net.IPNet { + return net.IPNet(n) +} + +type APIConf struct { + EnabledP *bool `yaml:"enabled,omitempty" default:"false"` + AllowedNets []IPNet `yaml:"allowed subnets,omitempty" default:"[\"127.0.0.0/8\", \"::1/128\"]"` +} + +func (conf *APIConf) AllowedIPNets() (allowedIPNets []net.IPNet) { + for _, allowedIPNet := range conf.AllowedNets { + allowedIPNets = append(allowedIPNets, allowedIPNet.IPNet()) + } + return allowedIPNets +} + +func (conf *APIConf) Unmarshal(unmarshal func(interface{}) error) error { + if err := defaults.Set(conf); err != nil { + return err + } + return nil +} + +func (conf APIConf) Enabled() bool { + return BoolP(conf.EnabledP) +} diff --git a/internal/pkg/config/authentication.go b/internal/pkg/config/authentication.go new file mode 100644 index 00000000..0a67cf32 --- /dev/null +++ b/internal/pkg/config/authentication.go @@ -0,0 +1,72 @@ +package config + +import ( + "fmt" + "os" + + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "gopkg.in/yaml.v3" + + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +type User struct { + Name string `json:"name" yaml:"name"` + PasswordHash string `json:"password hash" yaml:"password hash"` +} + +type Authentication struct { + Users []User `json:"users" yaml:"users"` + userMap map[string]User `json:"-" yaml:"-"` +} + +func NewAuthentication() *Authentication { + auth := new(Authentication) + auth.userMap = make(map[string]User) + return auth +} + +func (auth *Authentication) ParseFromRaw(data []byte) error { + err := yaml.Unmarshal(data, auth) + if err != nil { + return err + } + if len(auth.Users) == 0 { + return fmt.Errorf("no record users") + } + for _, user := range auth.Users { + if _, ok := auth.userMap[user.Name]; ok { + return fmt.Errorf("duplicated user names") + } + auth.userMap[user.Name] = user + } + return nil +} + +func (auth *Authentication) Read(confFileName string) error { + if data, err := os.ReadFile(confFileName); err != nil { + return err + } else { + if err := auth.ParseFromRaw(data); err != nil { + return err + } + } + return nil +} + +var ( + UnauthorizedError = errors.New("Unauthorized") +) + +func (auth *Authentication) Authenticate(name, pass string) (*User, error) { + if user, ok := auth.userMap[name]; !ok { + return nil, UnauthorizedError + } else { + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(pass)); err != nil { + wwlog.Warn("%v\n", err) + return nil, UnauthorizedError + } + return &user, nil + } +} diff --git a/internal/pkg/config/buildconfig.go.in b/internal/pkg/config/buildconfig.go.in index b7cd2634..6e442d34 100644 --- a/internal/pkg/config/buildconfig.go.in +++ b/internal/pkg/config/buildconfig.go.in @@ -68,6 +68,10 @@ func (paths BuildConfig) NodesConf() string { return path.Join(paths.Sysconfdir, "warewulf", "nodes.conf") } +func (paths BuildConfig) AuthenticationConf() string { + return path.Join(paths.Sysconfdir, "warewulf", "auth.conf") +} + func (paths BuildConfig) OciBlobCachedir() string { return path.Join(paths.Cachedir, "warewulf") } diff --git a/internal/pkg/config/root.go b/internal/pkg/config/root.go index a79a0b57..45bf788f 100644 --- a/internal/pkg/config/root.go +++ b/internal/pkg/config/root.go @@ -32,6 +32,7 @@ type WarewulfYaml struct { Ipv6net string `yaml:"ipv6net,omitempty"` Fqdn string `yaml:"fqdn,omitempty"` Warewulf *WarewulfConf `yaml:"warewulf,omitempty"` + API *APIConf `yaml:"api,omitempty"` DHCP *DHCPConf `yaml:"dhcp,omitempty"` TFTP *TFTPConf `yaml:"tftp,omitempty"` NFS *NFSConf `yaml:"nfs,omitempty"` @@ -55,6 +56,7 @@ func New() *WarewulfYaml { cachedConf.NFS = new(NFSConf) cachedConf.SSH = new(SSHConf) cachedConf.Paths = new(BuildConfig) + cachedConf.API = new(APIConf) if err := defaults.Set(&cachedConf); err != nil { panic(err) } diff --git a/internal/pkg/config/root_test.go b/internal/pkg/config/root_test.go index 3763e8cc..bad61665 100644 --- a/internal/pkg/config/root_test.go +++ b/internal/pkg/config/root_test.go @@ -46,6 +46,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, "cidr": { @@ -87,6 +92,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, "cidr with conflicts": { @@ -130,6 +140,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, "ipv6 cidr": { @@ -170,6 +185,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, "ipv6 cidr conflict": { @@ -211,6 +231,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, "example": { @@ -288,6 +313,11 @@ tftp: "00:09": ipxe-snponly-x86_64.efi "00:0B": arm64-efi/snponly.efi systemd name: tftp +api: + enabled: false + allowed subnets: + - 127.0.0.0/8 + - ::1/128 `, }, } diff --git a/internal/pkg/image/config.go b/internal/pkg/image/config.go index 164a74f3..938d2bfc 100644 --- a/internal/pkg/image/config.go +++ b/internal/pkg/image/config.go @@ -31,3 +31,7 @@ func ImageParentDir() string { func ImageFile(name string) string { return path.Join(ImageParentDir(), name+".img") } + +func CompressedImageFile(name string) string { + return ImageFile(name) + ".gz" +} diff --git a/internal/pkg/image/util.go b/internal/pkg/image/util.go index 3baa337f..6519a457 100644 --- a/internal/pkg/image/util.go +++ b/internal/pkg/image/util.go @@ -3,6 +3,7 @@ package image import ( "os" "path/filepath" + "time" "github.com/pkg/errors" @@ -114,3 +115,27 @@ func DeleteImage(name string) error { func IsWriteAble(name string) bool { return !util.IsFile(filepath.Join(SourceDir(name), "readonly")) } + +func ImageSize(name string) int { + if img, err := os.Stat(ImageFile(name)); err == nil { + return int(img.Size()) + } else { + return 0 + } +} + +func ImageModTime(name string) time.Time { + if img, err := os.Stat(ImageFile(name)); err == nil { + return img.ModTime() + } else { + return time.Time{} + } +} + +func CompressedImageSize(name string) int { + if img, err := os.Stat(CompressedImageFile(name)); err == nil { + return int(img.Size()) + } else { + return 0 + } +} diff --git a/internal/pkg/node/checkconf.go b/internal/pkg/node/checkconf.go deleted file mode 100644 index 1c881d2e..00000000 --- a/internal/pkg/node/checkconf.go +++ /dev/null @@ -1,109 +0,0 @@ -package node - -import ( - "fmt" - "net" - "reflect" - "strconv" - "strings" - - "github.com/warewulf/warewulf/internal/pkg/wwtype" -) - -/* -Checks if for NodeConf all values can be parsed according to their type. -*/ -func (nodeConf *Node) Check() (err error) { - nodeInfoType := reflect.TypeOf(nodeConf) - nodeInfoVal := reflect.ValueOf(nodeConf) - return check(nodeInfoType, nodeInfoVal) -} - -func (profileConf *Profile) Check() (err error) { - profileInfoType := reflect.TypeOf(profileConf) - profileInfoVal := reflect.ValueOf(profileConf) - return check(profileInfoType, profileInfoVal) -} - -func check(infoType reflect.Type, infoVal reflect.Value) (err error) { - if !infoVal.Elem().IsValid() { - return nil - } - // now iterate of every field - for i := 0; i < infoVal.Elem().NumField(); i++ { - if !infoType.Elem().Field(i).IsExported() { - continue - } else if infoType.Elem().Field(i).Type.Kind() == reflect.String { - newFmt, err := checker(infoVal.Elem().Field(i).String(), infoType.Elem().Field(i).Tag.Get("type")) - if err != nil { - return fmt.Errorf("field: %s value:%s err: %s", infoType.Elem().Field(i).Name, infoVal.Elem().Field(i).String(), err) - } else if newFmt != "" { - infoVal.Elem().Field(i).SetString(newFmt) - } - } else if infoType.Elem().Field(i).Type.Kind() == reflect.Ptr && !infoVal.Elem().Field(i).IsNil() { - nestType := reflect.TypeOf(infoVal.Elem().Field(i).Interface()) - nestVal := reflect.ValueOf(infoVal.Elem().Field(i).Interface()) - for j := 0; j < nestType.Elem().NumField(); j++ { - if nestType.Elem().Field(j).Type.Kind() == reflect.String { - newFmt, err := checker(nestVal.Elem().Field(j).Interface().(string), nestType.Elem().Field(j).Tag.Get("type")) - if err != nil { - return fmt.Errorf("field: %s value:%s err: %s", nestType.Elem().Field(j).Name, nestVal.Elem().Field(j).String(), err) - } else if newFmt != "" { - nestVal.Elem().Field(j).SetString(newFmt) - } - } - } - } else if infoType.Elem().Field(i).Type == reflect.TypeOf(map[string]*NetDev(nil)) { - netMap := infoVal.Elem().Field(i).Interface().(map[string]*NetDev) - for _, val := range netMap { - netType := reflect.TypeOf(val) - netVal := reflect.ValueOf(val) - for j := 0; j < netType.Elem().NumField(); j++ { - newFmt, err := checker(netVal.Elem().Field(j).String(), netType.Elem().Field(j).Tag.Get("type")) - if err != nil { - return fmt.Errorf("field: %s value:%s err: %s", netType.Elem().Field(j).Name, netVal.Elem().Field(j).String(), err) - } else if newFmt != "" { - netVal.Elem().Field(j).SetString(newFmt) - } - } - } - } - } - return nil -} - -func checker(value string, valType string) (niceValue string, err error) { - if valType == "" || value == "" || wwtype.IsUnsetVerb(value) { - return "", nil - } - switch valType { - case "": - return "", nil - case "bool": - if strings.ToLower(value) == "yes" { - return "true", nil - } - if strings.ToLower(value) == "no" { - return "false", nil - } - myBool, err := strconv.ParseBool(value) - return strconv.FormatBool(myBool), err - case "IP": - if addr := net.ParseIP(value); addr == nil { - return "", fmt.Errorf("%s can't be parsed to ip address", value) - } else { - return addr.String(), nil - } - case "MAC": - if mac, err := net.ParseMAC(value); err != nil { - return "", fmt.Errorf("%s can't be parsed to MAC address: %s", value, err) - } else { - return mac.String(), nil - } - case "uint": - if _, err := strconv.ParseUint(value, 10, 64); err != nil { - return "", fmt.Errorf("%s is not a uint: %s", value, err) - } - } - return "", nil -} diff --git a/internal/pkg/node/constructors.go b/internal/pkg/node/constructors.go index e819ad2a..73162e91 100644 --- a/internal/pkg/node/constructors.go +++ b/internal/pkg/node/constructors.go @@ -166,28 +166,6 @@ func (config *NodesYaml) FindAllProfiles(nodes ...string) (profileList []Profile return profileList, nil } -/* -Return the names of all available nodes -*/ -func (config *NodesYaml) ListAllNodes() []string { - nodeList := make([]string, len(config.Nodes)) - for name := range config.Nodes { - nodeList = append(nodeList, name) - } - return nodeList -} - -/* -Return the names of all available nodes -*/ -func (config *NodesYaml) ListAllProfiles() []string { - var nodeList []string - for name := range config.NodeProfiles { - nodeList = append(nodeList, name) - } - return nodeList -} - /* FindDiscoverableNode returns the first discoverable node and an interface to associate with the discovered interface. If the nodUNDEFe has diff --git a/internal/pkg/node/datastructure.go b/internal/pkg/node/datastructure.go index 6dcc1485..5fa2c8f4 100644 --- a/internal/pkg/node/datastructure.go +++ b/internal/pkg/node/datastructure.go @@ -6,8 +6,6 @@ import ( "github.com/warewulf/warewulf/internal/pkg/wwtype" ) -const undef string = "UNDEF" - /****** * YAML data representations ******/ @@ -26,8 +24,8 @@ type Node struct { id string valid bool // Is set true, if called by the constructor // exported values - Discoverable wwtype.WWbool `yaml:"discoverable,omitempty" lopt:"discoverable" sopt:"e" comment:"Make discoverable in given network (true/false)"` - AssetKey string `yaml:"asset key,omitempty" lopt:"asset" comment:"Set the node's Asset tag (key)"` + Discoverable wwtype.WWbool `yaml:"discoverable,omitempty" json:"discoverable,omitempty" lopt:"discoverable" sopt:"e" comment:"Make discoverable in given network (true/false)"` + AssetKey string `yaml:"asset key,omitempty" json:"asset key,omitempty" lopt:"asset" comment:"Set the node's Asset tag (key)"` Profile `yaml:"-,inline"` // include all values set in the profile, but inline them in yaml output if these are part of Node } @@ -37,56 +35,56 @@ Holds the data which can be set for profiles and nodes. type Profile struct { id string // exported values - Profiles []string `yaml:"profiles,omitempty" lopt:"profile" sopt:"P" comment:"Set the node's profile members (comma separated)"` - Comment string `yaml:"comment,omitempty" lopt:"comment" comment:"Set arbitrary string comment"` - ClusterName string `yaml:"cluster name,omitempty" lopt:"cluster" sopt:"c" comment:"Set cluster group"` - ImageName string `yaml:"image name,omitempty" lopt:"image" comment:"Set image name"` - Ipxe string `yaml:"ipxe template,omitempty" lopt:"ipxe" comment:"Set the iPXE template name"` - RuntimeOverlay []string `yaml:"runtime overlay,omitempty" lopt:"runtime-overlays" sopt:"R" comment:"Set the runtime overlay"` - SystemOverlay []string `yaml:"system overlay,omitempty" lopt:"system-overlays" sopt:"O" comment:"Set the system overlay"` - Kernel *KernelConf `yaml:"kernel,omitempty"` - Ipmi *IpmiConf `yaml:"ipmi,omitempty"` - Init string `yaml:"init,omitempty" lopt:"init" sopt:"i" comment:"Define the init process to boot the image"` - Root string `yaml:"root,omitempty" lopt:"root" comment:"Define the rootfs" ` - NetDevs map[string]*NetDev `yaml:"network devices,omitempty"` - Tags map[string]string `yaml:"tags,omitempty"` - PrimaryNetDev string `yaml:"primary network,omitempty" lopt:"primarynet" sopt:"p" comment:"Set the primary network interface"` - Disks map[string]*Disk `yaml:"disks,omitempty"` - FileSystems map[string]*FileSystem `yaml:"filesystems,omitempty"` - Resources map[string]Resource `yaml:"resources,omitempty"` + Profiles []string `yaml:"profiles,omitempty" json:"profiles,omitempty" lopt:"profile" sopt:"P" comment:"Set the node's profile members (comma separated)"` + Comment string `yaml:"comment,omitempty" json:"comment,omitempty" lopt:"comment" comment:"Set arbitrary string comment"` + ClusterName string `yaml:"cluster name,omitempty" json:"cluster name,omitempty" lopt:"cluster" sopt:"c" comment:"Set cluster group"` + ImageName string `yaml:"image name,omitempty" json:"image name,omitempty" lopt:"image" comment:"Set image name"` + Ipxe string `yaml:"ipxe template,omitempty" json:"ipxe template,omitempty" lopt:"ipxe" comment:"Set the iPXE template name"` + RuntimeOverlay []string `yaml:"runtime overlay,omitempty" json:"runtime overlay,omitempty" lopt:"runtime-overlays" sopt:"R" comment:"Set the runtime overlay"` + SystemOverlay []string `yaml:"system overlay,omitempty" json:"system overlay,omitempty" lopt:"system-overlays" sopt:"O" comment:"Set the system overlay"` + Kernel *KernelConf `yaml:"kernel,omitempty" json:"kernel,omitempty"` + Ipmi *IpmiConf `yaml:"ipmi,omitempty" json:"ipmi,omitempty"` + Init string `yaml:"init,omitempty" json:"init,omitempty" lopt:"init" sopt:"i" comment:"Define the init process to boot the image"` + Root string `yaml:"root,omitempty" json:"root,omitempty" lopt:"root" comment:"Define the rootfs" ` + NetDevs map[string]*NetDev `yaml:"network devices,omitempty" json:"network devices,omitempty"` + Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"` + PrimaryNetDev string `yaml:"primary network,omitempty" json:"primary network,omitempty" lopt:"primarynet" sopt:"p" comment:"Set the primary network interface"` + Disks map[string]*Disk `yaml:"disks,omitempty" json:"disks,omitempty"` + FileSystems map[string]*FileSystem `yaml:"filesystems,omitempty" json:"filesystems,omitempty"` + Resources map[string]Resource `yaml:"resources,omitempty" json:"resources,omitempty"` } type IpmiConf struct { - UserName string `yaml:"username,omitempty" lopt:"ipmiuser" comment:"Set the IPMI username"` - Password string `yaml:"password,omitempty" lopt:"ipmipass" comment:"Set the IPMI password"` - Ipaddr net.IP `yaml:"ipaddr,omitempty" lopt:"ipmiaddr" comment:"Set the IPMI IP address" type:"IP"` - Gateway net.IP `yaml:"gateway,omitempty" lopt:"ipmigateway" comment:"Set the IPMI gateway" type:"IP"` - Netmask net.IP `yaml:"netmask,omitempty" lopt:"ipminetmask" comment:"Set the IPMI netmask" type:"IP"` - Port string `yaml:"port,omitempty" lopt:"ipmiport" comment:"Set the IPMI port"` - Interface string `yaml:"interface,omitempty" lopt:"ipmiinterface" comment:"Set the node's IPMI interface (defaults: 'lan')"` - EscapeChar string `yaml:"escapechar,omitempty" lopt:"ipmiescapechar" comment:"Set the IPMI escape character (defaults: '~')"` - Write wwtype.WWbool `yaml:"write,omitempty" lopt:"ipmiwrite" comment:"Enable the write of impi configuration (true/false)"` - Template string `yaml:"template,omitempty" lopt:"ipmitemplate" comment:"template used for ipmi command"` - Tags map[string]string `yaml:"tags,omitempty"` + UserName string `yaml:"username,omitempty" json:"username,omitempty" lopt:"ipmiuser" comment:"Set the IPMI username"` + Password string `yaml:"password,omitempty" json:"password,omitempty" lopt:"ipmipass" comment:"Set the IPMI password"` + Ipaddr net.IP `yaml:"ipaddr,omitempty" json:"ipaddr,omitempty" lopt:"ipmiaddr" comment:"Set the IPMI IP address" type:"IP"` + Gateway net.IP `yaml:"gateway,omitempty" json:"gateway,omitempty" lopt:"ipmigateway" comment:"Set the IPMI gateway" type:"IP"` + Netmask net.IP `yaml:"netmask,omitempty" json:"netmask,omitempty" lopt:"ipminetmask" comment:"Set the IPMI netmask" type:"IP"` + Port string `yaml:"port,omitempty" json:"port,omitempty" lopt:"ipmiport" comment:"Set the IPMI port"` + Interface string `yaml:"interface,omitempty" json:"interface,omitempty" lopt:"ipmiinterface" comment:"Set the node's IPMI interface (defaults: 'lan')"` + EscapeChar string `yaml:"escapechar,omitempty" json:"escapechar,omitempty" lopt:"ipmiescapechar" comment:"Set the IPMI escape character (defaults: '~')"` + Write wwtype.WWbool `yaml:"write,omitempty" json:"write,omitempty" lopt:"ipmiwrite" comment:"Enable the write of impi configuration (true/false)"` + Template string `yaml:"template,omitempty" json:"template,omitempty" lopt:"ipmitemplate" comment:"template used for ipmi command"` + Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"` } type KernelConf struct { - Version string `yaml:"version,omitempty" lopt:"kernelversion" comment:"Set kernel version" json:"version,omitempty"` - Args []string `yaml:"args,omitempty" lopt:"kernelargs" sopt:"A" comment:"Set kernel arguments" json:"args,omitempty"` + Version string `yaml:"version,omitempty" json:"version,omitempty" lopt:"kernelversion" comment:"Set kernel version"` + Args []string `yaml:"args,omitempty" json:"args,omitempty" lopt:"kernelargs" sopt:"A" comment:"Set kernel arguments"` } type NetDev struct { - Type string `yaml:"type,omitempty" lopt:"type" sopt:"T" comment:"Set device type of given network"` - OnBoot wwtype.WWbool `yaml:"onboot,omitempty" lopt:"onboot" comment:"Enable/disable network device (true/false)"` - Device string `yaml:"device,omitempty" lopt:"netdev" sopt:"N" comment:"Set the device for given network"` - Hwaddr string `yaml:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network" type:"MAC"` - Ipaddr net.IP `yaml:"ipaddr,omitempty" comment:"IPv4 address in given network" sopt:"I" lopt:"ipaddr" type:"IP"` - Ipaddr6 net.IP `yaml:"ip6addr,omitempty" lopt:"ipaddr6" comment:"IPv6 address" type:"IP"` - Prefix net.IP `yaml:"prefix,omitempty"` - Netmask net.IP `yaml:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask" type:"IP"` - Gateway net.IP `yaml:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's network device gateway" type:"IP"` - MTU string `yaml:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu" type:"uint"` - Tags map[string]string `yaml:"tags,omitempty"` + Type string `yaml:"type,omitempty" json:"type,omitempty" lopt:"type" sopt:"T" comment:"Set device type of given network"` + OnBoot wwtype.WWbool `yaml:"onboot,omitempty" json:"onbot,omitempty" lopt:"onboot" comment:"Enable/disable network device (true/false)"` + Device string `yaml:"device,omitempty" json:"device,omitempty" lopt:"netdev" sopt:"N" comment:"Set the device for given network"` + Hwaddr string `yaml:"hwaddr,omitempty" json:"hwaddr,omitempty" lopt:"hwaddr" sopt:"H" comment:"Set the device's HW address for given network" type:"MAC"` + Ipaddr net.IP `yaml:"ipaddr,omitempty" json:"ipaddr,omitempty" lopt:"ipaddr" sopt:"I" comment:"IPv4 address in given network" type:"IP"` + Ipaddr6 net.IP `yaml:"ip6addr,omitempty" json:"ip6addr,omitempty" lopt:"ipaddr6" comment:"IPv6 address" type:"IP"` + Prefix net.IP `yaml:"prefix,omitempty" json:"prefix,omitempty"` + Netmask net.IP `yaml:"netmask,omitempty" json:"netmask,omitempty" lopt:"netmask" sopt:"M" comment:"Set the networks netmask" type:"IP"` + Gateway net.IP `yaml:"gateway,omitempty" json:"gateway,omitempty" lopt:"gateway" sopt:"G" comment:"Set the node's network device gateway" type:"IP"` + MTU string `yaml:"mtu,omitempty" json:"mtu,omitempty" lopt:"mtu" comment:"Set the mtu" type:"uint"` + Tags map[string]string `yaml:"tags,omitempty" json:"tags,omitempty"` primary bool } @@ -94,8 +92,8 @@ type NetDev struct { Holds the disks of a node */ type Disk struct { - WipeTable bool `yaml:"wipe_table,omitempty" lopt:"diskwipe" comment:"whether or not the partition tables shall be wiped"` - Partitions map[string]*Partition `yaml:"partitions,omitempty"` + WipeTable bool `yaml:"wipe_table,omitempty" json:"wipe_table,omitempty" lopt:"diskwipe" comment:"whether or not the partition tables shall be wiped"` + Partitions map[string]*Partition `yaml:"partitions,omitempty" json:"partitions,omitempty"` } /* @@ -103,27 +101,27 @@ partition definition, the label must be uniq so its used as the key in the Partitions map */ type Partition struct { - Number string `yaml:"number,omitempty" lopt:"partnumber" comment:"set the partition number, if not set next free slot is used" type:"uint"` - SizeMiB string `yaml:"size_mib,omitempty" lopt:"partsize" comment:"set the size of the partition, if not set maximal possible size is used" type:"uint"` - StartMiB string `yaml:"start_mib,omitempty" comment:"the start of the partition" type:"uint"` - TypeGuid string `yaml:"type_guid,omitempty" comment:"Linux filesystem data will be used if empty"` - Guid string `yaml:"guid,omitempty" comment:"the GPT unique partition GUID"` - WipePartitionEntry bool `yaml:"wipe_partition_entry,omitempty" comment:"if true, Ignition will clobber an existing partition if it does not match the config"` - ShouldExist bool `yaml:"should_exist,omitempty" lopt:"partcreate" comment:"create partition if not exist"` - Resize bool `yaml:"resize,omitempty" comment:" whether or not the existing partition should be resize"` + Number string `yaml:"number,omitempty" json:"number,omitempty" lopt:"partnumber" comment:"set the partition number, if not set next free slot is used" type:"uint"` + SizeMiB string `yaml:"size_mib,omitempty" json:"size_mib,omitempty" lopt:"partsize" comment:"set the size of the partition, if not set maximal possible size is used" type:"uint"` + StartMiB string `yaml:"start_mib,omitempty" json:"start_mib,omitempty" comment:"the start of the partition" type:"uint"` + TypeGuid string `yaml:"type_guid,omitempty" json:"type_guid,omitempty" comment:"Linux filesystem data will be used if empty"` + Guid string `yaml:"guid,omitempty" json:"guid,omitempty" comment:"the GPT unique partition GUID"` + WipePartitionEntry bool `yaml:"wipe_partition_entry,omitempty" json:"wipe_partition_entry,omitempty" comment:"if true, Ignition will clobber an existing partition if it does not match the config"` + ShouldExist bool `yaml:"should_exist,omitempty" json:"should_exist,omitempty" lopt:"partcreate" comment:"create partition if not exist"` + Resize bool `yaml:"resize,omitempty" json:"resize,omitempty" comment:"whether or not the existing partition should be resize"` } /* Definition of a filesystem. The device is uniq so its used as key */ type FileSystem struct { - Format string `yaml:"format,omitempty" lopt:"fsformat" comment:"format of the file system"` - Path string `yaml:"path,omitempty" lopt:"fspath" comment:"the mount point of the file system"` - WipeFileSystem bool `yaml:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"` - Label string `yaml:"label,omitempty" comment:"the label of the filesystem"` - Uuid string `yaml:"uuid,omitempty" comment:"the uuid of the filesystem"` - Options []string `yaml:"options,omitempty" comment:"any additional options to be passed to the format-specific mkfs utility"` - MountOptions string `yaml:"mount_options,omitempty" comment:"any special options to be passed to the mount command"` + Format string `yaml:"format,omitempty" json:"format,omitempty" lopt:"fsformat" comment:"format of the file system"` + Path string `yaml:"path,omitempty" json:"path,omitempty" lopt:"fspath" comment:"the mount point of the file system"` + WipeFileSystem bool `yaml:"wipe_filesystem,omitempty" json:"wipe_filesystem,omitempty" lopt:"fswipe" comment:"wipe file system at boot"` + Label string `yaml:"label,omitempty" json:"label,omitempty" comment:"the label of the filesystem"` + Uuid string `yaml:"uuid,omitempty" json:"uuid,omitempty" comment:"the uuid of the filesystem"` + Options []string `yaml:"options,omitempty" json:"options,omitempty" comment:"any additional options to be passed to the format-specific mkfs utility"` + MountOptions string `yaml:"mount_options,omitempty" json:"mount_options,omitempty" comment:"any special options to be passed to the mount command"` } type Resource interface{} diff --git a/internal/pkg/node/methods.go b/internal/pkg/node/methods.go index a7b28cc3..7a9eaf1b 100644 --- a/internal/pkg/node/methods.go +++ b/internal/pkg/node/methods.go @@ -201,7 +201,7 @@ func recursiveFlatten(obj interface{}) (hasContent bool) { if typeObj.Elem().Field(i).Type == reflect.TypeOf([]string{}) { del := false for _, elem := range (valObj.Elem().Field(i).Interface()).([]string) { - if strings.EqualFold(elem, undef) { + if isUnsetValue(elem) { del = true } } @@ -213,7 +213,7 @@ func recursiveFlatten(obj interface{}) (hasContent bool) { hasContent = true } case reflect.String: - if strings.EqualFold(valObj.Elem().Field(i).String(), undef) { + if isUnsetValue(valObj.Elem().Field(i).String()) { valObj.Elem().Field(i).SetString("") } if valObj.Elem().Field(i).String() != "" { @@ -356,7 +356,7 @@ func (node *Node) updatePrimaryNetDev() { } sort.Strings(keys) if len(keys) > 0 { - wwlog.Debug("%s: no primary defined, sanitizing to: %s", node.id, keys[0]) + wwlog.Debug("%s: no primary netdev defined, sanitizing to: %s", node.id, keys[0]) node.NetDevs[keys[0]].primary = true node.PrimaryNetDev = keys[0] } @@ -421,3 +421,9 @@ func (netdev *NetDev) IpCIDR() string { } return ipCIDR.String() } + +var unsetValues = []string{"UNSET", "UNDEF"} + +func isUnsetValue(value string) bool { + return util.InSlice(unsetValues, value) +} diff --git a/internal/pkg/node/queries.go b/internal/pkg/node/queries.go new file mode 100644 index 00000000..1cd4ffc4 --- /dev/null +++ b/internal/pkg/node/queries.go @@ -0,0 +1,104 @@ +package node + +import "slices" + +// ListAllNodes returns a slice of all node names defined in the Nodes map. +func (config *NodesYaml) ListAllNodes() []string { + nodes := []string{} + for n := range config.Nodes { + nodes = append(nodes, n) + } + slices.Sort(nodes) + return nodes +} + +// ListAllProfiles returns a slice of all profile IDs defined in the +// NodeProfiles map. +func (config *NodesYaml) ListAllProfiles() []string { + profiles := []string{} + for p := range config.NodeProfiles { + profiles = append(profiles, p) + } + slices.Sort(profiles) + return profiles +} + +// ListNodesUsingProfile returns a slice of node IDs that reference the +// specified profileID. +func (config *NodesYaml) ListNodesUsingProfile(profileID string) []string { + nodes := []string{} + for n := range config.Nodes { + if slices.Contains(config.Nodes[n].Profiles, profileID) { + nodes = append(nodes, n) + } + } + slices.Sort(nodes) + return nodes +} + +// ListProfilesUsingProfile returns a slice of profile IDs from NodeProfiles +// that reference the specified profileID. +func (config *NodesYaml) ListProfilesUsingProfile(profileID string) []string { + profiles := []string{} + for p := range config.NodeProfiles { + if slices.Contains(config.NodeProfiles[p].Profiles, profileID) { + profiles = append(profiles, p) + } + } + slices.Sort(profiles) + return profiles +} + +// ListNodesUsingImage returns a slice of node IDs for nodes that use the +// specified image. +func (config *NodesYaml) ListNodesUsingImage(image string) []string { + nodes := []string{} + for n := range config.Nodes { + if config.Nodes[n].ImageName == image { + nodes = append(nodes, n) + } + } + slices.Sort(nodes) + return nodes +} + +// ListProfilesUsingImage returns a slice of profile IDs for profiles that use +// the specified image. +func (config *NodesYaml) ListProfilesUsingImage(image string) []string { + profiles := []string{} + for p := range config.NodeProfiles { + if config.NodeProfiles[p].ImageName == image { + profiles = append(profiles, p) + } + } + slices.Sort(profiles) + return profiles +} + +// ListNodesUsingOverlay returns a slice of node IDs for nodes that include the +// specified overlay in either RuntimeOverlay or SystemOverlay. +func (config *NodesYaml) ListNodesUsingOverlay(overlay string) []string { + nodes := []string{} + for n := range config.Nodes { + if slices.Contains(config.Nodes[n].RuntimeOverlay, overlay) || + slices.Contains(config.Nodes[n].SystemOverlay, overlay) { + nodes = append(nodes, n) + } + } + slices.Sort(nodes) + return nodes +} + +// ListProfilesUsingOverlay returns a slice of profile IDs for profiles that +// include the specified overlay in either RuntimeOverlay or SystemOverlay. +func (config *NodesYaml) ListProfilesUsingOverlay(overlay string) []string { + profiles := []string{} + for p := range config.NodeProfiles { + if slices.Contains(config.NodeProfiles[p].RuntimeOverlay, overlay) || + slices.Contains(config.NodeProfiles[p].SystemOverlay, overlay) { + profiles = append(profiles, p) + } + } + slices.Sort(profiles) + return profiles +} diff --git a/internal/pkg/node/queries_test.go b/internal/pkg/node/queries_test.go new file mode 100644 index 00000000..8ff5793a --- /dev/null +++ b/internal/pkg/node/queries_test.go @@ -0,0 +1,395 @@ +package node + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ListAllNodes(t *testing.T) { + tests := map[string]struct { + registry string + nodes []string + }{ + "empty": { + registry: ``, + nodes: []string{}, + }, + "one node": { + registry: ` +nodes: + n1: {}`, + nodes: []string{"n1"}, + }, + "multiple nodes": { + registry: ` +nodes: + n1: {} + n2: {}`, + nodes: []string{"n1", "n2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.nodes, registry.ListAllNodes()) + }) + } +} + +func Test_ListAllProfiles(t *testing.T) { + tests := map[string]struct { + registry string + profiles []string + }{ + "empty": { + registry: ``, + profiles: []string{}, + }, + "one profile": { + registry: ` +nodeprofiles: + p1: {}`, + profiles: []string{"p1"}, + }, + "multiple profiles": { + registry: ` +nodeprofiles: + p1: {} + p2: {}`, + profiles: []string{"p1", "p2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.profiles, registry.ListAllProfiles()) + }) + } +} + +func Test_ListNodesUsingProfile(t *testing.T) { + tests := map[string]struct { + registry string + profile string + nodes []string + }{ + "empty": { + registry: ``, + profile: "p1", + nodes: []string{}, + }, + "node without profle": { + registry: ` +nodes: + n1: {}`, + profile: "p1", + nodes: []string{}, + }, + "node with profle": { + registry: ` +nodes: + n1: + profiles: + - p1`, + profile: "p1", + nodes: []string{"n1"}, + }, + "multiple nodes one with profile": { + registry: ` +nodes: + n1: {} + n2: + profiles: + - p1`, + profile: "p1", + nodes: []string{"n2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.nodes, registry.ListNodesUsingProfile(tt.profile)) + }) + } +} + +func Test_ListProfilesUsingProfile(t *testing.T) { + tests := map[string]struct { + registry string + profile string + profiles []string + }{ + "empty": { + registry: ``, + profile: "p1", + profiles: []string{}, + }, + "profile without profle": { + registry: ` +nodeprofiles: + p1: {}`, + profile: "p2", + profiles: []string{}, + }, + "profile with profile": { + registry: ` +nodeprofiles: + p1: + profiles: + - p2`, + profile: "p2", + profiles: []string{"p1"}, + }, + "multiple profiles one with profile": { + registry: ` +nodeprofiles: + p1: {} + p2: + profiles: + - p3`, + profile: "p3", + profiles: []string{"p2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.profiles, registry.ListProfilesUsingProfile(tt.profile)) + }) + } +} + +func Test_ListNodesUsingImage(t *testing.T) { + tests := map[string]struct { + registry string + image string + nodes []string + }{ + "empty": { + registry: ``, + image: "i1", + nodes: []string{}, + }, + "node without image": { + registry: ` +nodes: + n1: {}`, + image: "i1", + nodes: []string{}, + }, + "node with image": { + registry: ` +nodes: + n1: + image name: i1`, + image: "i1", + nodes: []string{"n1"}, + }, + "multiple nodes one with profile": { + registry: ` +nodes: + n1: {} + n2: + image name: i1`, + image: "i1", + nodes: []string{"n2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.nodes, registry.ListNodesUsingImage(tt.image)) + }) + } +} + +func Test_ListProfilesUsingImage(t *testing.T) { + tests := map[string]struct { + registry string + image string + profiles []string + }{ + "empty": { + registry: ``, + image: "i1", + profiles: []string{}, + }, + "profile without image": { + registry: ` +nodeprofiles: + p1: {}`, + image: "i1", + profiles: []string{}, + }, + "profile with image": { + registry: ` +nodeprofiles: + p1: + image name: i1`, + image: "i1", + profiles: []string{"p1"}, + }, + "multiple profiles one with image": { + registry: ` +nodeprofiles: + p1: {} + p2: + image name: i1`, + image: "i1", + profiles: []string{"p2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.profiles, registry.ListProfilesUsingImage(tt.image)) + }) + } +} + +func Test_ListNodesUsingOverlay(t *testing.T) { + tests := map[string]struct { + registry string + overlay string + nodes []string + }{ + "empty": { + registry: ``, + overlay: "o1", + nodes: []string{}, + }, + "node without profle": { + registry: ` +nodes: + n1: {}`, + overlay: "o1", + nodes: []string{}, + }, + "node with runtime overlay": { + registry: ` +nodes: + n1: + runtime overlay: + - o1`, + overlay: "o1", + nodes: []string{"n1"}, + }, + "multiple nodes one with runtime overlay": { + registry: ` +nodes: + n1: {} + n2: + runtime overlay: + - o1`, + overlay: "o1", + nodes: []string{"n2"}, + }, + "node with system overlay": { + registry: ` +nodes: + n1: + system overlay: + - o1`, + overlay: "o1", + nodes: []string{"n1"}, + }, + "multiple nodes one with system overlay": { + registry: ` +nodes: + n1: {} + n2: + system overlay: + - o1`, + overlay: "o1", + nodes: []string{"n2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.nodes, registry.ListNodesUsingOverlay(tt.overlay)) + }) + } +} + +func Test_ListProfilesUsingOverlay(t *testing.T) { + tests := map[string]struct { + registry string + overlay string + profiles []string + }{ + "empty": { + registry: ``, + overlay: "o1", + profiles: []string{}, + }, + "node without profle": { + registry: ` +nodeprofiles: + p1: {}`, + overlay: "o1", + profiles: []string{}, + }, + "node with runtime overlay": { + registry: ` +nodeprofiles: + p1: + runtime overlay: + - o1`, + overlay: "o1", + profiles: []string{"p1"}, + }, + "multiple nodes one with runtime overlay": { + registry: ` +nodeprofiles: + p1: {} + p2: + runtime overlay: + - o1`, + overlay: "o1", + profiles: []string{"p2"}, + }, + "node with system overlay": { + registry: ` +nodeprofiles: + p1: + system overlay: + - o1`, + overlay: "o1", + profiles: []string{"p1"}, + }, + "multiple nodes one with system overlay": { + registry: ` +nodeprofiles: + p1: {} + p2: + system overlay: + - o1`, + overlay: "o1", + profiles: []string{"p2"}, + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + registry, err := Parse([]byte(tt.registry)) + assert.NoError(t, err) + assert.Equal(t, tt.profiles, registry.ListProfilesUsingOverlay(tt.overlay)) + }) + } +} diff --git a/internal/pkg/overlay/overlay.go b/internal/pkg/overlay/overlay.go index 3b76ae4f..4b9d80d1 100644 --- a/internal/pkg/overlay/overlay.go +++ b/internal/pkg/overlay/overlay.go @@ -110,12 +110,18 @@ func BuildAllOverlays(nodes []node.Node, allNodes []node.Node, workerCount int) for n := range nodeChan { wwlog.Info("Building system overlay image for %s", n.Id()) wwlog.Debug("System overlays for %s: [%s]", n.Id(), strings.Join(n.SystemOverlay, ", ")) + if len(n.SystemOverlay) < 1 { + wwlog.Warn("No system overlays defined for %s", n.Id()) + } if err := BuildOverlay(n, allNodes, "system", n.SystemOverlay); err != nil { errChan <- fmt.Errorf("could not build system overlays %v for node %s: %w", n.SystemOverlay, n.Id(), err) } wwlog.Info("Building runtime overlay image for %s", n.Id()) wwlog.Debug("Runtime overlays for %s: [%s]", n.Id(), strings.Join(n.RuntimeOverlay, ", ")) + if len(n.RuntimeOverlay) < 1 { + wwlog.Warn("No runtime overlays defined for %s", n.Id()) + } if err := BuildOverlay(n, allNodes, "runtime", n.RuntimeOverlay); err != nil { errChan <- fmt.Errorf("could not build runtime overlays %v for node %s: %w", n.RuntimeOverlay, n.Id(), err) } diff --git a/internal/pkg/upgrade/node.go b/internal/pkg/upgrade/node.go index 353856c5..4bdab94f 100644 --- a/internal/pkg/upgrade/node.go +++ b/internal/pkg/upgrade/node.go @@ -126,7 +126,7 @@ func (legacy *NodesYaml) Upgrade(addDefaults bool, replaceOverlays bool, warewul defaultProfile.Kernel = new(node.KernelConf) } if len(defaultProfile.Kernel.Args) < 1 { - defaultProfile.Kernel.Args = []string{"quiet", "crashkernel=no"} + defaultProfile.Kernel.Args = []string{"quiet", "crashkernel=no", "net.ifnames=1"} } if defaultProfile.Init == "" { defaultProfile.Init = "/sbin/init" @@ -329,18 +329,10 @@ func (legacy *Node) Upgrade(addDefaults bool, replaceOverlays bool) (upgraded *n } } if replaceOverlays { - if indexOf(upgraded.SystemOverlay, "wwinit") != -1 { - upgraded.SystemOverlay = replaceSliceElement( - upgraded.SystemOverlay, - indexOf(upgraded.SystemOverlay, "wwinit"), - wwinitSplitOverlays) - } - if indexOf(upgraded.RuntimeOverlay, "generic") != -1 { - upgraded.RuntimeOverlay = replaceSliceElement( - upgraded.RuntimeOverlay, - indexOf(upgraded.RuntimeOverlay, "generic"), - genericSplitOverlays) - } + upgraded.SystemOverlay = replaceOverlay( + upgraded.SystemOverlay, "wwinit", wwinitSplitOverlays) + upgraded.RuntimeOverlay = replaceOverlay( + upgraded.RuntimeOverlay, "generic", genericSplitOverlays) } if legacy.Resources != nil { for key, value := range legacy.Resources { @@ -535,18 +527,10 @@ func (legacy *Profile) Upgrade(addDefaults bool, replaceOverlays bool) (upgraded } } if replaceOverlays { - if indexOf(upgraded.SystemOverlay, "wwinit") != -1 { - upgraded.SystemOverlay = replaceSliceElement( - upgraded.SystemOverlay, - indexOf(upgraded.SystemOverlay, "wwinit"), - wwinitSplitOverlays) - } - if indexOf(upgraded.RuntimeOverlay, "generic") != -1 { - upgraded.RuntimeOverlay = replaceSliceElement( - upgraded.RuntimeOverlay, - indexOf(upgraded.RuntimeOverlay, "generic"), - genericSplitOverlays) - } + upgraded.SystemOverlay = replaceOverlay( + upgraded.SystemOverlay, "wwinit", wwinitSplitOverlays) + upgraded.RuntimeOverlay = replaceOverlay( + upgraded.RuntimeOverlay, "generic", genericSplitOverlays) } if legacy.Resources != nil { for key, value := range legacy.Resources { diff --git a/internal/pkg/upgrade/node_test.go b/internal/pkg/upgrade/node_test.go index 4db27cc4..5f476b10 100644 --- a/internal/pkg/upgrade/node_test.go +++ b/internal/pkg/upgrade/node_test.go @@ -578,6 +578,7 @@ nodeprofiles: args: - quiet - crashkernel=no + - net.ifnames=1 init: /sbin/init root: initramfs resources: @@ -658,6 +659,7 @@ nodeprofiles: args: - quiet - crashkernel=no + - net.ifnames=1 init: /sbin/init root: initramfs resources: @@ -676,7 +678,7 @@ nodes: `, }, { - name: "replace overlays conflicts", + name: "replace overlays", addDefaults: false, replaceOverlays: true, legacyYaml: ` @@ -736,6 +738,99 @@ nodes: - debian.interfaces - wicked - ignition +`, + }, + { + name: "replace overlays again", + addDefaults: false, + replaceOverlays: true, + legacyYaml: ` +nodeprofiles: + default: + runtime overlay: + - hosts + - ssh.authorized_keys + - syncuser + system overlay: + - wwinit + - wwclient + - fstab + - hostname + - ssh.host_keys + - issue + - resolv + - udev.netname + - systemd.netname + - ifcfg + - NetworkManager + - debian.interfaces + - wicked + - ignition +nodes: + n1: + runtime overlay: + - hosts + - ssh.authorized_keys + - syncuser + system overlay: + - wwinit + - wwclient + - fstab + - hostname + - ssh.host_keys + - issue + - resolv + - udev.netname + - systemd.netname + - ifcfg + - NetworkManager + - debian.interfaces + - wicked + - ignition +`, + upgradedYaml: ` +nodeprofiles: + default: + runtime overlay: + - hosts + - ssh.authorized_keys + - syncuser + system overlay: + - wwinit + - wwclient + - fstab + - hostname + - ssh.host_keys + - issue + - resolv + - udev.netname + - systemd.netname + - ifcfg + - NetworkManager + - debian.interfaces + - wicked + - ignition +nodes: + n1: + runtime overlay: + - hosts + - ssh.authorized_keys + - syncuser + system overlay: + - wwinit + - wwclient + - fstab + - hostname + - ssh.host_keys + - issue + - resolv + - udev.netname + - systemd.netname + - ifcfg + - NetworkManager + - debian.interfaces + - wicked + - ignition `, }, { @@ -921,6 +1016,7 @@ nodeprofiles: args: - quiet - crashkernel=no + - net.ifnames=1 init: /sbin/init root: initramfs resources: diff --git a/internal/pkg/upgrade/slices.go b/internal/pkg/upgrade/slices.go index 780350d3..e6bbf484 100644 --- a/internal/pkg/upgrade/slices.go +++ b/internal/pkg/upgrade/slices.go @@ -15,3 +15,26 @@ func replaceSliceElement[T any](original []T, index int, replacement []T) []T { } return append(original[:index], append(replacement, original[index+1:]...)...) } + +func replaceOverlay(originals []string, toReplace string, replacements []string) []string { + if indexOf(originals, toReplace) == -1 { + return originals + } + + lookup := make(map[string]bool) + for _, v := range originals { + lookup[v] = true + } + + var newReplacements []string + for _, v := range replacements { + if !lookup[v] || v == toReplace { + newReplacements = append(newReplacements, v) + } + } + + return replaceSliceElement( + originals, + indexOf(originals, toReplace), + newReplacements) +} diff --git a/internal/pkg/warewulfd/api/api.go b/internal/pkg/warewulfd/api/api.go new file mode 100644 index 00000000..9e214bdc --- /dev/null +++ b/internal/pkg/warewulfd/api/api.go @@ -0,0 +1,80 @@ +package api + +import ( + "net" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/swaggest/openapi-go/openapi3" + "github.com/swaggest/rest/nethttp" + "github.com/swaggest/rest/web" + swgui "github.com/swaggest/swgui/v5emb" + + "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/version" +) + +func Handler(auth *config.Authentication, allowedNets []net.IPNet) *web.Service { + api := web.NewService(openapi3.NewReflector()) + + api.OpenAPISchema().SetTitle("Warewulf v4 API") + api.OpenAPISchema().SetDescription("This service provides an API to a Warewulf v4 server.") + api.OpenAPISchema().SetVersion(version.GetVersion()) + + api.Route("/api/nodes", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(AuthMiddleware(auth, allowedNets)) + + r.Method(http.MethodGet, "/", nethttp.NewHandler(getNodes())) + r.Method(http.MethodGet, "/{id}", nethttp.NewHandler(getNodeByID())) + r.Method(http.MethodGet, "/{id}/raw", nethttp.NewHandler(getRawNodeByID())) + r.Method(http.MethodPut, "/{id}", nethttp.NewHandler(addNode())) + r.Method(http.MethodDelete, "/{id}", nethttp.NewHandler(deleteNode())) + r.Method(http.MethodPatch, "/{id}", nethttp.NewHandler(updateNode())) + r.Method(http.MethodGet, "/{id}/fields", nethttp.NewHandler(getNodeFields())) + r.Method(http.MethodPost, "/overlays/build", nethttp.NewHandler(buildAllOverlays())) + r.Method(http.MethodPost, "/{id}/overlays/build", nethttp.NewHandler(buildOverlays())) + }) + }) + + api.Route("/api/profiles", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(AuthMiddleware(auth, allowedNets)) + + r.Method(http.MethodGet, "/", nethttp.NewHandler(getProfiles())) + r.Method(http.MethodGet, "/{id}", nethttp.NewHandler(getProfileByID())) + r.Method(http.MethodPut, "/{id}", nethttp.NewHandler(addProfile())) + r.Method(http.MethodPatch, "/{id}", nethttp.NewHandler(updateProfile())) + r.Method(http.MethodDelete, "/{id}", nethttp.NewHandler(deleteProfile())) + }) + }) + + api.Route("/api/images", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(AuthMiddleware(auth, allowedNets)) + + r.Method(http.MethodGet, "/", nethttp.NewHandler(getImages())) + r.Method(http.MethodGet, "/{name}", nethttp.NewHandler(getImageByName())) + r.Method(http.MethodPost, "/{name}/import", nethttp.NewHandler(importImage())) + r.Method(http.MethodPatch, "/{name}", nethttp.NewHandler(updateImage())) + r.Method(http.MethodPost, "/{name}/build", nethttp.NewHandler(buildImage())) + r.Method(http.MethodDelete, "/{name}", nethttp.NewHandler(deleteImage())) + }) + }) + + api.Route("/api/overlays", func(r chi.Router) { + r.Group(func(r chi.Router) { + r.Use(AuthMiddleware(auth, allowedNets)) + + r.Method(http.MethodGet, "/", nethttp.NewHandler(getOverlays())) + r.Method(http.MethodGet, "/{name}", nethttp.NewHandler(getOverlayByName())) + r.Method(http.MethodGet, "/{name}/file", nethttp.NewHandler(getOverlayFile())) + r.Method(http.MethodPut, "/{name}", nethttp.NewHandler(createOverlay())) + r.Method(http.MethodDelete, "/{name}", nethttp.NewHandler(deleteOverlay())) + }) + }) + + api.Docs("/api/docs", swgui.New) + + return api +} diff --git a/internal/pkg/warewulfd/api/authentication.go b/internal/pkg/warewulfd/api/authentication.go new file mode 100644 index 00000000..5b6711a2 --- /dev/null +++ b/internal/pkg/warewulfd/api/authentication.go @@ -0,0 +1,56 @@ +package api + +import ( + "fmt" + "net" + "net/http" + + "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func AuthMiddleware(auth *config.Authentication, allowedNets []net.IPNet) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wwlog.Debug("allowed subnets: %v", allowedNets) + wwlog.Debug("remote address: %v", r.RemoteAddr) + fromAllowedNet := false + if ipStr, _, err := net.SplitHostPort(r.RemoteAddr); err == nil { + ip := net.ParseIP(ipStr) + if ip == nil { + http.Error(w, fmt.Sprintf("Invalid remote address: %v", r.RemoteAddr), http.StatusForbidden) + } + for _, allowedNet := range allowedNets { + if allowedNet.Contains(ip) { + fromAllowedNet = true + break + } + } + if !fromAllowedNet { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + } else { + http.Error(w, fmt.Sprintf("Invalid remote address: %v", r.RemoteAddr), http.StatusForbidden) + return + } + + if auth != nil { + username, password, ok := r.BasicAuth() + if !ok { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + _, err := auth.Authenticate(username, password) + if err != nil { + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + } + next.ServeHTTP(w, r) + }) + } +} diff --git a/internal/pkg/warewulfd/api/image.go b/internal/pkg/warewulfd/api/image.go new file mode 100644 index 00000000..4156f196 --- /dev/null +++ b/internal/pkg/warewulfd/api/image.go @@ -0,0 +1,213 @@ +package api + +import ( + "context" + "fmt" + "strings" + + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + image_api "github.com/warewulf/warewulf/internal/pkg/api/image" + "github.com/warewulf/warewulf/internal/pkg/api/routes/wwapiv1" + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/kernel" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +type Image struct { + Kernels []string `json:"kernels"` + Size int `json:"size"` + BuildTime int64 `json:"buildtime"` + Writable bool `json:"writable"` +} + +func NewImage(name string) *Image { + c := new(Image) + c.Kernels = []string{} + for _, k := range kernel.FindKernels(name) { + c.Kernels = append(c.Kernels, k.Path) + } + c.Size = image.ImageSize(name) + modTime := image.ImageModTime(name) + if modTime.IsZero() { + c.BuildTime = 0 + } else { + c.BuildTime = modTime.Unix() + } + c.Writable = image.IsWriteAble(name) + return c +} + +func getImages() usecase.Interactor { + u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*Image) error { + wwlog.Debug("api.getImages()") + m := make(map[string]*Image) + if names, err := image.ListSources(); err != nil { + return err + } else { + for _, name := range names { + m[name] = NewImage(name) + } + *output = m + return nil + } + }) + u.SetTitle("Get images") + u.SetDescription("Get all node images") + u.SetTags("Image") + return u +} + +func getImageByName() usecase.Interactor { + type getImageByNameInput struct { + Name string `path:"name" required:"true" description:"Name of image to add"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getImageByNameInput, output *Image) error { + wwlog.Debug("api.getImageByName(Name:%v)", input.Name) + if !image.ValidSource(input.Name) { + return status.Wrap(fmt.Errorf("image not found: %v", input.Name), status.NotFound) + } else { + *output = *NewImage(input.Name) + return nil + } + }) + u.SetTitle("Get an image") + u.SetDescription("Get a node image by its name") + u.SetTags("Image") + return u +} + +func importImage() usecase.Interactor { + type importImageInput struct { + Name string `path:"name" required:"true" description:"Name of image to import"` + URI string `json:"uri" required:"true" description:"OCI registry URI to import image definition from"` + NoHttps bool `json:"nohttps" default:"false" description:"Use http, rather than https, to communicate with the registry, default:'false'"` + User string `json:"user" description:"Username for the registry, if needed"` + Password string `json:"password" description:"Password for the registry, if needed"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input importImageInput, output *Image) error { + wwlog.Debug("api.importImage(Name:%v, URI:%v, NoHttps:%v, User:%v, Password:[redacted])", + input.Name, input.URI, input.NoHttps, input.User) + if !strings.HasPrefix(input.URI, "docker://") { + return status.Wrap(fmt.Errorf("missing docker:// prefix: %s", input.URI), status.InvalidArgument) + } + + if !image.ValidName(input.Name) { + return status.Wrap(fmt.Errorf("name contains illegal characters: %s", input.Name), status.InvalidArgument) + } + + if sctx, err := image_api.GetSystemContext(input.NoHttps, input.User, input.Password, ""); err != nil { + return err + } else { + if err := image.ImportDocker(input.URI, input.Name, sctx); err != nil { + return err + } + *output = *NewImage(input.Name) + return nil + } + }) + u.SetTitle("Import an image") + u.SetDescription("Import a node image from an OCI registry") + u.SetTags("Image") + + return u +} + +func deleteImage() usecase.Interactor { + type deleteImageInput struct { + Name string `path:"name" required:"true" description:"Name of image to delete"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input deleteImageInput, output *Image) error { + wwlog.Debug("api.deleteImage(Name:%v)", input.Name) + if image.ValidSource(input.Name) { + *output = *NewImage(input.Name) + } + + if registry, err := node.New(); err != nil { + return err + } else { + nodesCount := len(registry.ListNodesUsingImage(input.Name)) + profilesCount := len(registry.ListProfilesUsingImage(input.Name)) + if nodesCount > 0 || profilesCount > 0 { + return status.Wrap(fmt.Errorf( + "image '%s' is in use by %v nodes and %v profiles", input.Name, nodesCount, profilesCount), + status.InvalidArgument) + } + } + + cdp := &wwapiv1.ImageDeleteParameter{ + ImageNames: []string{input.Name}, + } + + return image_api.ImageDelete(cdp) + }) + u.SetTitle("Delete an image") + u.SetDescription("Delete an existing node image") + u.SetTags("Image") + + return u +} + +func updateImage() usecase.Interactor { + type renameImageInput struct { + Name string `path:"name" required:"true" description:"Name of image to update"` + NewName string `json:"name" description:"New name to rename the image to"` + Build bool `query:"build" default:"true" description:"Build the image image after renaming, default:'true'"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input renameImageInput, output *Image) error { + wwlog.Debug("api.updateImage(Name:%v, NewName:%v, Build:%v)", input.Name, input.NewName, input.Build) + name := input.Name + if input.NewName != "" { + crp := &wwapiv1.ImageRenameParameter{ + ImageName: input.Name, + TargetName: input.NewName, + Build: input.Build, + } + + if err := image_api.ImageRename(crp); err != nil { + return err + } + name = input.NewName + } + + *output = *NewImage(name) + return nil + }) + u.SetTitle("Update or rename an image") + u.SetDescription("Update or rename an existing node image") + u.SetTags("Image") + + return u +} + +func buildImage() usecase.Interactor { + type buildImageInput struct { + Name string `path:"name" required:"true" description:"Name of image to build"` + Force bool `query:"force" default:"false" description:"Build the image image even if it appears unnecessary, default:'false'"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input buildImageInput, output *Image) error { + wwlog.Debug("api.buildImage(Name:%v, Force:%v)", input.Name, input.Force) + cbp := &wwapiv1.ImageBuildParameter{ + ImageNames: []string{input.Name}, + Force: input.Force, + } + + if err := image_api.ImageBuild(cbp); err != nil { + return err + } + + *output = *NewImage(input.Name) + return nil + }) + u.SetTitle("Build an image") + u.SetDescription("Build a node image") + u.SetTags("Image") + + return u +} diff --git a/internal/pkg/warewulfd/api/image_test.go b/internal/pkg/warewulfd/api/image_test.go new file mode 100644 index 00000000..eef2c7b6 --- /dev/null +++ b/internal/pkg/warewulfd/api/image_test.go @@ -0,0 +1,144 @@ +package api + +import ( + "bytes" + "encoding/json" + "io" + "net" + "net/http" + "net/http/httptest" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/testenv" +) + +func TestImageAPI(t *testing.T) { + env := testenv.New(t) + defer env.RemoveAll() + + authData := ` +users: +- name: admin + password hash: $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC +` + auth := config.NewAuthentication() + err := auth.ParseFromRaw([]byte(authData)) + assert.NoError(t, err) + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(auth, allowedNets)) + defer srv.Close() + env.WriteFile(path.Join(testenv.WWChrootdir, "test-image/rootfs/file"), `test`) + + t.Run("test no authentication", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.Equal(t, resp.StatusCode, http.StatusUnauthorized) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.Equal(t, "Unauthorized\n", string(body)) + }) + + t.Run("test get all images", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images", nil) + assert.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"test-image": {"kernels":[], "size":0, "buildtime":0, "writable":true}}`, string(body)) + }) + + t.Run("test get single image", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/images/test-image", nil) + assert.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernels":[] ,"size":0, "buildtime":0, "writable":true}`, string(body)) + }) + + t.Run("test build image", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/images/test-image/build?force=true&default=true", nil) + assert.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + + var bodyData map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) + assert.True(t, bodyData["buildtime"].(float64) > 0.0) + + bodyData["buildtime"] = 0.0 + assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) + }) + + t.Run("test rename image", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/images/test-image?build=true", bytes.NewBuffer([]byte(`{"name": "new-image"}`))) + assert.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + + var bodyData map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) + assert.True(t, bodyData["buildtime"].(float64) > 0.0) + + bodyData["buildtime"] = 0.0 + assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) + }) + + t.Run("test delete image", func(t *testing.T) { + req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/images/new-image", nil) + assert.NoError(t, err) + req.SetBasicAuth("admin", "admin") + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.Equal(t, resp.StatusCode, http.StatusOK) + assert.NoError(t, err) + + var bodyData map[string]interface{} + assert.NoError(t, json.Unmarshal([]byte(body), &bodyData)) + assert.True(t, bodyData["buildtime"].(float64) > 0.0) + + bodyData["buildtime"] = 0.0 + assert.Equal(t, map[string]interface{}{"kernels": []interface{}{}, "size": 512.0, "buildtime": 0.0, "writable": true}, bodyData) + }) +} diff --git a/internal/pkg/warewulfd/api/node.go b/internal/pkg/warewulfd/api/node.go new file mode 100644 index 00000000..ba8f120d --- /dev/null +++ b/internal/pkg/warewulfd/api/node.go @@ -0,0 +1,303 @@ +package api + +import ( + "context" + "fmt" + "runtime" + "sort" + + "dario.cat/mergo" + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/overlay" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func getNodes() usecase.Interactor { + u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*node.Node) error { + wwlog.Debug("api.getNodes()") + if registry, err := node.New(); err != nil { + return err + } else { + nodeMap := make(map[string]*node.Node) + if nodeList, err := registry.FindAllNodes(); err != nil { + return err + } else { + for i := range nodeList { + nodeMap[nodeList[i].Id()] = &nodeList[i] + } + *output = nodeMap + return nil + } + } + }) + u.SetTitle("Get nodes") + u.SetDescription("Get all nodes, including field values from associated profiles.") + u.SetTags("Node") + return u +} + +func getNodeByID() usecase.Interactor { + type getNodeByIDInput struct { + ID string `path:"id" required:"true" description:"ID of node to get"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getNodeByIDInput, output *node.Node) error { + wwlog.Debug("api.getNodeByID(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if node_, err := registry.GetNode(input.ID); err != nil { + return status.Wrap(fmt.Errorf("node not found: %v (%v)", input.ID, err), status.NotFound) + } else { + *output = node_ + return nil + } + } + }) + u.SetTitle("Get a node") + u.SetDescription("Get a node by its ID, including field values from associated profiles.") + u.SetTags("Node") + u.SetExpectedErrors(status.NotFound) + return u +} + +func getRawNodeByID() usecase.Interactor { + type getNodeByIDInput struct { + ID string `path:"id" required:"true" description:"ID of node to get"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getNodeByIDInput, output *node.Node) error { + wwlog.Debug("api.getRawNodeByID(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if node_, ok := registry.Nodes[input.ID]; !ok { + return status.Wrap(fmt.Errorf("node not found: %v", input.ID), status.NotFound) + } else { + *output = *node_ + return nil + } + } + }) + u.SetTitle("Get a raw node") + u.SetDescription("Get a node by its ID, without field values from associated profiles.") + u.SetTags("Node") + u.SetExpectedErrors(status.NotFound) + return u +} + +func getNodeFields() usecase.Interactor { + type getNodeByIDInput struct { + ID string `path:"id" required:"true" description:"ID of node from which to retrieve fields"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getNodeByIDInput, output *[]node.Field) error { + wwlog.Debug("api.getNodeFields(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if n, fields, err := registry.MergeNode(input.ID); err != nil { + return status.Wrap(fmt.Errorf("node not found: %v (%v)", input.ID, err), status.NotFound) + } else { + *output = fields.List(n) + return nil + } + } + }) + u.SetTitle("Get node fields") + u.SetDescription("Get the fields and values of a node, indicating which profiles each field originates from.") + u.SetTags("Node") + u.SetExpectedErrors(status.NotFound) + return u +} + +func addNode() usecase.Interactor { + type addNodeInput struct { + ID string `path:"id" required:"true" description:"ID of node to be added"` + Node node.Node `json:"node" required:"true" description:"Field values in JSON format for added node"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input addNodeInput, output *node.Node) error { + wwlog.Debug("api.addNode(ID:%v, Node:%+v)", input.ID, input.Node) + if registry, err := node.New(); err != nil { + return err + } else { + for _, profile := range input.Node.Profiles { + if _, ok := registry.NodeProfiles[profile]; !ok { + return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument) + } + } + if input.Node.ImageName != "" && !image.ValidSource(input.Node.ImageName) { + return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument) + } + for _, overlay_ := range input.Node.SystemOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + for _, overlay_ := range input.Node.RuntimeOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + registry.Nodes[input.ID] = &input.Node + if err := registry.Persist(); err != nil { + return err + } + warewulfd.Reload() + *output = *(registry.Nodes[input.ID]) + return nil + } + }) + u.SetTitle("Add a node") + u.SetDescription("Add a new node.") + u.SetTags("Node") + + return u +} + +func deleteNode() usecase.Interactor { + type deleteNodeInput struct { + ID string `path:"id" required:"true" description:"ID of node to delete"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input deleteNodeInput, output *node.Node) error { + wwlog.Debug("api.deleteNode(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if node, ok := registry.Nodes[input.ID]; ok { + *output = *node + } + if err := registry.DelNode(input.ID); err != nil { + return err + } + if err := registry.Persist(); err != nil { + return err + } + warewulfd.Reload() + return nil + } + }) + u.SetTitle("Delete a node") + u.SetDescription("Delete an existing node.") + u.SetTags("Node") + + return u +} + +func updateNode() usecase.Interactor { + type updateNodeInput struct { + ID string `path:"id" description:"ID of node to update"` + Node node.Node `json:"node" required:"true" description:"Field values in JSON format to update on node"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input updateNodeInput, output *node.Node) error { + wwlog.Debug("api.updateNode(ID:%v, Node:%+v)", input.ID, input.Node) + if registry, err := node.New(); err != nil { + return err + } else { + for _, profile := range input.Node.Profiles { + if _, ok := registry.NodeProfiles[profile]; !ok { + return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument) + } + } + if input.Node.ImageName != "" && !image.ValidSource(input.Node.ImageName) { + return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Node.ImageName), status.InvalidArgument) + } + for _, overlay_ := range input.Node.SystemOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + for _, overlay_ := range input.Node.RuntimeOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + if nodePtr, err := registry.GetNodeOnlyPtr(input.ID); err != nil { + return status.Wrap(err, status.NotFound) + } else { + if err := mergo.MergeWithOverwrite(nodePtr, &input.Node); err != nil { + return err + } + if err := registry.Persist(); err != nil { + return err + } + warewulfd.Reload() + *output = *nodePtr + return nil + } + } + }) + u.SetTitle("Update a node") + u.SetDescription("Update an existing node.") + u.SetTags("Node") + + return u +} + +func buildAllOverlays() usecase.Interactor { + u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *[]string) error { + wwlog.Debug("api.buildAllOverlays()") + if registry, err := node.New(); err != nil { + return err + } else { + if nodes, err := registry.FindAllNodes(); err != nil { + return fmt.Errorf("could not get node list: %w", err) + } else { + ret := make([]string, len(nodes)) + for i := range nodes { + ret[i] = nodes[i].Id() + } + sort.Strings(ret) + if err := overlay.BuildAllOverlays(nodes, nodes, runtime.NumCPU()); err != nil { + return err + } + *output = ret + return nil + } + } + }) + u.SetTitle("Build all overlay images") + u.SetDescription("Build system and runtime overlay images for all nodes.") + u.SetTags("Node") + + return u +} + +func buildOverlays() usecase.Interactor { + type buildOverlayInput struct { + ID string `path:"id" description:"ID of node to build overlay images for"` + } + u := usecase.NewInteractor(func(ctx context.Context, input *buildOverlayInput, output *string) error { + wwlog.Debug("api.buildOverlays()") + if registry, err := node.New(); err != nil { + return err + } else { + nodes, err := registry.FindAllNodes() + if err != nil { + return err + } + + if node_, err := registry.GetNode(input.ID); err != nil { + return status.Wrap(err, status.NotFound) + } else { + if err := overlay.BuildAllOverlays([]node.Node{node_}, nodes, runtime.NumCPU()); err != nil { + return err + } + *output = input.ID + return nil + } + } + }) + u.SetTitle("Build overlay images for a node") + u.SetDescription("Build system and runtime overlay images for a node.") + u.SetTags("Node") + + return u +} diff --git a/internal/pkg/warewulfd/api/node_test.go b/internal/pkg/warewulfd/api/node_test.go new file mode 100644 index 00000000..92c72045 --- /dev/null +++ b/internal/pkg/warewulfd/api/node_test.go @@ -0,0 +1,176 @@ +package api + +import ( + "bytes" + "io" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" +) + +func TestNodeAPI(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + t.Run("add a node", func(t *testing.T) { + // prepareration + + testNode := `{ + "node":{ + "kernel": { + "version": "v1.0.0", + "args": ["kernel-args"] + } + } +}` + req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(testNode))) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("read all nodes", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes", nil) + assert.NoError(t, err) + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"node1": {}, "test": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) + }) + + t.Run("get one specific node", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil) + assert.NoError(t, err) + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("update the node", func(t *testing.T) { + updateNode := `{ + "node":{ + "kernel": { + "version": "v1.0.1-newversion" + } + } +}` + req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/nodes/test", bytes.NewBuffer([]byte(updateNode))) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("get one specific node (again)", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("get one specific (raw) node", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/nodes/test/raw", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("test build all nodes overlays", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/overlays/build", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `["node1", "test"]`, string(body)) + }) + + t.Run("test build one node's overlays", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPost, srv.URL+"/api/nodes/test/overlays/build", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `"test"`, string(body)) + }) + + t.Run("test delete nodes", func(t *testing.T) { + req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/nodes/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) +} diff --git a/internal/pkg/warewulfd/api/overlay.go b/internal/pkg/warewulfd/api/overlay.go new file mode 100644 index 00000000..f18f534d --- /dev/null +++ b/internal/pkg/warewulfd/api/overlay.go @@ -0,0 +1,234 @@ +package api + +import ( + "context" + "fmt" + "net/url" + "os" + "path" + + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/overlay" + "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +type OverlayResponse struct { + Files []string `json:"files"` + Site bool `json:"site"` +} + +func NewOverlayResponse(name string) *OverlayResponse { + o := new(OverlayResponse) + o.Files = []string{} + if files, err := overlay.OverlayGetFiles(name); err == nil { + o.Files = files + } + o.Site = overlay.GetOverlay(name).IsSiteOverlay() + return o +} + +func getOverlays() usecase.Interactor { + u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*OverlayResponse) error { + wwlog.Debug("api.getOverlays()") + m := make(map[string]*OverlayResponse) + names := overlay.FindOverlays() + for _, name := range names { + m[name] = NewOverlayResponse(name) + } + *output = m + return nil + }) + u.SetTitle("Get overlays") + u.SetDescription("Get all overlays.") + u.SetTags("Overlay") + return u +} + +func getOverlayByName() usecase.Interactor { + type getOverlayByNameInput struct { + Name string `path:"name" required:"true" description:"Name of overlay to get"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getOverlayByNameInput, output *OverlayResponse) error { + wwlog.Debug("api.getOverlayByName(Name:%v)", input.Name) + if !overlay.GetOverlay(input.Name).Exists() { + return status.Wrap(fmt.Errorf("overlay not found: %v", input.Name), status.NotFound) + } else { + *output = *NewOverlayResponse(input.Name) + return nil + } + }) + u.SetTitle("Get an overlay") + u.SetDescription("Get an overlay by its name.") + u.SetTags("Overlay") + return u +} + +type OverlayFile struct { + Overlay string `json:"overlay"` + Path string `json:"path"` + Contents string `json:"contents"` + rendered bool +} + +func (of *OverlayFile) FullPath() string { + return path.Join(overlay.GetOverlay(of.Overlay).Rootfs(), of.Path) +} + +func (of *OverlayFile) Exists() bool { + return overlay.GetOverlay(of.Overlay).Exists() && util.IsFile(of.FullPath()) +} + +func (of *OverlayFile) readContents() (string, error) { + f, err := os.ReadFile(of.FullPath()) + return string(f), err +} + +func (of *OverlayFile) renderContents(nodeName string) (string, error) { + if !(path.Ext(of.Path) == ".ww") { + return "", fmt.Errorf("'%s' does not end with '.ww'", of.Path) + } + + if of.rendered { + return "", fmt.Errorf("already rendered") + } + + registry, regErr := node.New() + if regErr != nil { + return "", regErr + } + + renderNode, nodeErr := registry.GetNode(nodeName) + if nodeErr != nil { + return "", nodeErr + } + + allNodes, allNodesErr := registry.FindAllNodes() + if allNodesErr != nil { + return "", allNodesErr + } + + tstruct, structErr := overlay.InitStruct(of.Overlay, renderNode, allNodes) + if structErr != nil { + return "", structErr + } + tstruct.BuildSource = of.Path + + buffer, _, _, renderErr := overlay.RenderTemplateFile(of.FullPath(), tstruct) + if renderErr != nil { + return "", renderErr + } + + return buffer.String(), nil +} + +func NewOverlayFile(name string, path string, renderNodeName string) (*OverlayFile, error) { + of := new(OverlayFile) + of.Overlay = name + of.Path = path + if renderNodeName == "" { + if contents, err := of.readContents(); err != nil { + return of, err + } else { + of.Contents = contents + } + } else { + if contents, err := of.renderContents(renderNodeName); err != nil { + return of, err + } else { + of.Contents = contents + } + } + return of, nil +} + +func getOverlayFile() usecase.Interactor { + type getOverlayFileInput struct { + Name string `path:"name" required:"true" description:"Name of overlay to get a file from"` + Path string `query:"path" required:"true" description:"Path to file to get from an overlay"` + Node string `query:"render" description:"ID of node to render a template for"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getOverlayFileInput, output *OverlayFile) error { + wwlog.Debug("api.getOverlayFile(Name:%v, Path:%v, Node:%v)", input.Name, input.Path, input.Node) + if input.Path == "" { + return status.Wrap(fmt.Errorf("must specify a path"), status.InvalidArgument) + } + + if relPath, err := url.QueryUnescape(input.Path); err != nil { + return fmt.Errorf("failed to decode path: %v: %w", input.Path, err) + } else { + if overlayFile, err := NewOverlayFile(input.Name, relPath, input.Node); err != nil { + return fmt.Errorf("unable to read overlay file %v: %v: %w", input.Name, relPath, err) + } else { + *output = *overlayFile + return nil + } + } + }) + u.SetTitle("Get a file from an overlay") + u.SetDescription("Get a file from an overlay from the overlay name and file path, optionally rendered for a given node.") + u.SetTags("Overlay") + return u +} + +func createOverlay() usecase.Interactor { + type createOverlayInput struct { + Name string `path:"name" required:"true" description:"Name of overlay to create"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input createOverlayInput, output *OverlayResponse) error { + wwlog.Debug("api.createOverlay(Name:%v)", input.Name) + newOverlay := overlay.GetSiteOverlay(input.Name) + if err := newOverlay.Create(); err != nil { + return err + } + *output = *NewOverlayResponse(newOverlay.Name()) + return nil + }) + u.SetTitle("Create an overlay") + u.SetDescription("Create an overlay.") + u.SetTags("Overlay") + return u +} + +func deleteOverlay() usecase.Interactor { + type deleteOverlayInput struct { + Name string `path:"name" required:"true" description:"Name of overlay to delete"` + Force bool `query:"force" default:"false" description:"Whether to delete a non-empty overlay, default:'false'"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input deleteOverlayInput, output *OverlayResponse) error { + wwlog.Debug("api.deleteOverlay(Name:%v, Force:%v)", input.Name, input.Force) + if registry, err := node.New(); err != nil { + return err + } else { + nodesCount := len(registry.ListNodesUsingOverlay(input.Name)) + profilesCount := len(registry.ListProfilesUsingOverlay(input.Name)) + if nodesCount > 0 || profilesCount > 0 { + return status.Wrap(fmt.Errorf( + "overlay '%s' is in use by %v nodes and %v profiles", input.Name, nodesCount, profilesCount), + status.InvalidArgument) + } + } + *output = *NewOverlayResponse(input.Name) + overlay_ := overlay.GetSiteOverlay(input.Name) + if input.Force { + if err := os.RemoveAll(overlay_.Path()); err != nil { + return err + } + } else { + if err := os.Remove(overlay_.Path()); err != nil { + return err + } + } + return nil + }) + u.SetTitle("Delete an overlay") + u.SetDescription("Delete an overlay.") + u.SetTags("Overlay") + return u +} diff --git a/internal/pkg/warewulfd/api/overlay_test.go b/internal/pkg/warewulfd/api/overlay_test.go new file mode 100644 index 00000000..2ddf98e5 --- /dev/null +++ b/internal/pkg/warewulfd/api/overlay_test.go @@ -0,0 +1,121 @@ +package api + +import ( + "io" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" +) + +func TestOverlayAPI(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + env.WriteFile("usr/share/warewulf/overlays/testoverlay/email.ww", ` +{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }} +`) + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + t.Run("get all overlays", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) + assert.NoError(t, err) + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"testoverlay":{"files":["/email.ww"], "site":false}}`, string(body)) + }) + + t.Run("get one specific overlay", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay", nil) + assert.NoError(t, err) + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"files":["/email.ww"], "site":false}`, string(body)) + }) + + t.Run("get overlay file", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays/testoverlay/file?path=email.ww", nil) + assert.NoError(t, err) + + // send request + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + // validate the resp + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"overlay":"testoverlay","path":"email.ww","contents":"\n{{ if .Tags.email }}eMail: {{ .Tags.email }}{{else}} noMail{{- end }}\n"}`, string(body)) + }) + + t.Run("create an overlay", func(t *testing.T) { + req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/overlays/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"files":null, "site":true}`, string(body)) + }) + + t.Run("get all overlays", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/overlays", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"test":{"files":null, "site":true},"testoverlay":{"files":["/email.ww"], "site":false}}`, string(body)) + }) + + t.Run("test delete overlays", func(t *testing.T) { + req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/overlays/test?force=true", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, err) + assert.NoError(t, resp.Body.Close()) + + assert.JSONEq(t, `{"files":null, "site":true}`, string(body)) + }) +} diff --git a/internal/pkg/warewulfd/api/profile.go b/internal/pkg/warewulfd/api/profile.go new file mode 100644 index 00000000..a62a9a03 --- /dev/null +++ b/internal/pkg/warewulfd/api/profile.go @@ -0,0 +1,193 @@ +package api + +import ( + "context" + "fmt" + + "dario.cat/mergo" + "github.com/swaggest/usecase" + "github.com/swaggest/usecase/status" + "github.com/warewulf/warewulf/internal/pkg/image" + "github.com/warewulf/warewulf/internal/pkg/node" + "github.com/warewulf/warewulf/internal/pkg/overlay" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +func getProfiles() usecase.Interactor { + u := usecase.NewInteractor(func(ctx context.Context, _ struct{}, output *map[string]*node.Profile) error { + wwlog.Debug("api.getProfiles()") + if registry, err := node.New(); err != nil { + return err + } else { + *output = registry.NodeProfiles + return nil + } + }) + u.SetTitle("Get profiles") + u.SetDescription("Get all node profiles.") + u.SetTags("Profile") + return u +} + +func getProfileByID() usecase.Interactor { + type getProfileByIDInput struct { + ID string `path:"id" required:"true" description:"ID of profile to get"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input getProfileByIDInput, output *node.Profile) error { + wwlog.Debug("api.getProfileByID(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if profile, err := registry.GetProfile(input.ID); err != nil { + return status.Wrap(fmt.Errorf("profile not found: %v (%v)", input.ID, err), status.NotFound) + } else { + *output = profile + return nil + } + } + }) + u.SetTitle("Get a profile") + u.SetDescription("Get a node profile by its ID.") + u.SetTags("Profile") + u.SetExpectedErrors(status.NotFound) + return u +} + +func addProfile() usecase.Interactor { + type addProfileInput struct { + ID string `path:"id" required:"true" description:"ID of profile to add"` + Profile node.Profile `json:"profile" required:"true" description:"Field values in JSON format for added profile"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input addProfileInput, output *node.Profile) error { + wwlog.Debug("api.addProfile(ID:%v, Profile:%+v)", input.ID, input.Profile) + if registry, err := node.New(); err != nil { + return err + } else { + for _, profile := range input.Profile.Profiles { + if _, ok := registry.NodeProfiles[profile]; !ok { + return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument) + } + } + if input.Profile.ImageName != "" && !image.ValidSource(input.Profile.ImageName) { + return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument) + } + for _, overlay_ := range input.Profile.SystemOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + for _, overlay_ := range input.Profile.RuntimeOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + registry.NodeProfiles[input.ID] = &input.Profile + if err := registry.Persist(); err != nil { + return err + } + warewulfd.Reload() + *output = *(registry.NodeProfiles[input.ID]) + return nil + } + }) + u.SetTitle("Add a profile") + u.SetDescription("Add a new node profile.") + u.SetTags("Profile") + + return u +} + +func updateProfile() usecase.Interactor { + type updateProfileInput struct { + ID string `path:"id" required:"true" description:"ID of profile to update"` + Profile node.Profile `json:"profile" required:"true" description:"Field values in JSON format to update on profile"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input updateProfileInput, output *node.Profile) error { + wwlog.Debug("api.updateProfile(ID:%v, Profile:%+v)", input.ID, input.Profile) + if registry, err := node.New(); err != nil { + return err + } else { + for _, profile := range input.Profile.Profiles { + if _, ok := registry.NodeProfiles[profile]; !ok { + return status.Wrap(fmt.Errorf("profile '%s' does not exist", profile), status.InvalidArgument) + } + } + if input.Profile.ImageName != "" && !image.ValidSource(input.Profile.ImageName) { + return status.Wrap(fmt.Errorf("image '%s' does not exist", input.Profile.ImageName), status.InvalidArgument) + } + for _, overlay_ := range input.Profile.SystemOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + for _, overlay_ := range input.Profile.RuntimeOverlay { + if !overlay.GetOverlay(overlay_).Exists() { + return status.Wrap(fmt.Errorf("overlay '%s' does not exist", overlay_), status.InvalidArgument) + } + } + if profilePtr, err := registry.GetProfilePtr(input.ID); err != nil { + return status.Wrap(err, status.NotFound) + } else { + if err := mergo.MergeWithOverwrite(profilePtr, &input.Profile); err != nil { + return err + } + if err := registry.Persist(); err != nil { + return err + } + warewulfd.Reload() + *output = *profilePtr + return nil + } + } + }) + u.SetTitle("Update a profile") + u.SetDescription("Update an existing node profile.") + u.SetTags("Profile") + + return u +} + +func deleteProfile() usecase.Interactor { + type deleteProfileInput struct { + ID string `path:"id" required:"true" description:"ID of profile to delete"` + } + + u := usecase.NewInteractor(func(ctx context.Context, input deleteProfileInput, output *node.Profile) error { + wwlog.Debug("api.deleteProfile(ID:%v)", input.ID) + if registry, err := node.New(); err != nil { + return err + } else { + if profile, ok := registry.NodeProfiles[input.ID]; ok { + *output = *profile + } + + nodesCount := len(registry.ListNodesUsingProfile(input.ID)) + profilesCount := len(registry.ListProfilesUsingProfile(input.ID)) + if nodesCount > 0 || profilesCount > 0 { + return status.Wrap(fmt.Errorf( + "profile '%s' is in use by %v nodes and %v profiles", input.ID, nodesCount, profilesCount), + status.InvalidArgument) + } + + if err := registry.DelProfile(input.ID); err != nil { + return err + } + + if err := registry.Persist(); err != nil { + return err + } + + warewulfd.Reload() + return nil + } + }) + u.SetTitle("Delete a profile") + u.SetDescription("Delete an existing node profile.") + u.SetTags("Profile") + + return u +} diff --git a/internal/pkg/warewulfd/api/profile_test.go b/internal/pkg/warewulfd/api/profile_test.go new file mode 100644 index 00000000..68e2754e --- /dev/null +++ b/internal/pkg/warewulfd/api/profile_test.go @@ -0,0 +1,122 @@ +package api + +import ( + "bytes" + "io" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/warewulf/warewulf/internal/pkg/testenv" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" +) + +func TestProfileAPI(t *testing.T) { + warewulfd.SetNoDaemon() + env := testenv.New(t) + defer env.RemoveAll() + + allowedNets := []net.IPNet{ + { + IP: net.IPv4(127, 0, 0, 0), + Mask: net.CIDRMask(8, 32), + }, + } + srv := httptest.NewServer(Handler(nil, allowedNets)) + defer srv.Close() + + t.Run("get all profiles", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"default": {}}`, string(body)) + }) + + t.Run("add a new profile", func(t *testing.T) { + testProfile := `{"profile": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}` + req, err := http.NewRequest(http.MethodPut, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(testProfile))) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("re-read all profiles", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"default": {}, "test": {"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}}`, string(body)) + }) + + t.Run("get one specific profile (that was just added)", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernel": {"version": "v1.0.0", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("update the profile", func(t *testing.T) { + updateProfile := `{"profile": {"kernel": {"version": "v1.0.1-newversion"}}}` + req, err := http.NewRequest(http.MethodPatch, srv.URL+"/api/profiles/test", bytes.NewBuffer([]byte(updateProfile))) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("get one specific profile (that was just updated)", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/profiles/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) + + t.Run("test delete a profile", func(t *testing.T) { + req, err := http.NewRequest(http.MethodDelete, srv.URL+"/api/profiles/test", nil) + assert.NoError(t, err) + + resp, err := http.DefaultTransport.RoundTrip(req) + assert.NoError(t, err) + + body, err := io.ReadAll(resp.Body) + assert.NoError(t, resp.Body.Close()) + assert.NoError(t, err) + assert.JSONEq(t, `{"kernel": {"version": "v1.0.1-newversion", "args": ["kernel-args"]}}`, string(body)) + }) +} diff --git a/internal/pkg/warewulfd/daemon.go b/internal/pkg/warewulfd/daemon.go index b6825300..5ee29d42 100644 --- a/internal/pkg/warewulfd/daemon.go +++ b/internal/pkg/warewulfd/daemon.go @@ -49,8 +49,6 @@ func DaemonInitLogging() error { if err == nil { wwlog.SetLogLevel(level) } - } else { - wwlog.SetLogLevel(wwlog.INFO) } loginit = true diff --git a/internal/pkg/warewulfd/nodedb.go b/internal/pkg/warewulfd/nodedb.go index 4467be44..47322470 100644 --- a/internal/pkg/warewulfd/nodedb.go +++ b/internal/pkg/warewulfd/nodedb.go @@ -106,3 +106,13 @@ func GetNodeOrSetDiscoverable(hwaddr string) (node.Node, error) { // return the discovered node return db.yml.GetNode(nodeFound.Id()) } + +func Reload() { + if err := LoadNodeDB(); err != nil { + wwlog.Error("Could not load node DB: %s", err) + } + + if err := LoadNodeStatus(); err != nil { + wwlog.Error("Could not prepopulate node status DB: %s", err) + } +} diff --git a/internal/pkg/warewulfd/provision.go b/internal/pkg/warewulfd/provision.go index 07e261dd..83608d0e 100644 --- a/internal/pkg/warewulfd/provision.go +++ b/internal/pkg/warewulfd/provision.go @@ -97,7 +97,11 @@ func ProvisionSend(w http.ResponseWriter, req *http.Request) { } } else if rinfo.stage == "ipxe" { - stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/ipxe/"+remoteNode.Ipxe+".ipxe") + template := remoteNode.Ipxe + if template == "" { + template = "default" + } + stage_file = path.Join(conf.Paths.Sysconfdir, "warewulf/ipxe", template+".ipxe") kernelArgs := "" kernelVersion := "" if remoteNode.Kernel != nil { diff --git a/internal/pkg/warewulfd/server/server.go b/internal/pkg/warewulfd/server/server.go new file mode 100644 index 00000000..26bac7f8 --- /dev/null +++ b/internal/pkg/warewulfd/server/server.go @@ -0,0 +1,90 @@ +package server + +import ( + "fmt" + "net/http" + "os" + "os/signal" + "strconv" + "strings" + "syscall" + + warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" + "github.com/warewulf/warewulf/internal/pkg/util" + "github.com/warewulf/warewulf/internal/pkg/warewulfd" + "github.com/warewulf/warewulf/internal/pkg/warewulfd/api" + "github.com/warewulf/warewulf/internal/pkg/wwlog" +) + +// TODO: https://github.com/danderson/netboot/blob/master/pixiecore/dhcp.go +// TODO: https://github.com/pin/tftp +/* +wrapper type for the server mux as shim requests http://efiboot//grub.efi +which is filtered out by http to `301 Moved Permanently` what +shim.efi can't handle. So filter out `//` before they hit go/http. +Makes go/http more to behave like apache +*/ +type slashFix struct { + mux http.Handler +} + +/* +Filter out the '//' +*/ +func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) { + r.URL.Path = strings.Replace(r.URL.Path, "//", "/", -1) + h.mux.ServeHTTP(w, r) +} + +func defaultHandler() *slashFix { + var wwHandler http.ServeMux + wwHandler.HandleFunc("/provision/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/ipxe/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/efiboot/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/kernel/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/container/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/overlay-system/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/overlay-runtime/", warewulfd.ProvisionSend) + wwHandler.HandleFunc("/overlay-file/", warewulfd.OverlaySend) + wwHandler.HandleFunc("/status", warewulfd.StatusSend) + return &slashFix{&wwHandler} +} + +func RunServer() error { + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + + go func() { + for range c { + wwlog.Info("Received SIGHUP, reloading...") + warewulfd.Reload() + } + }() + + warewulfd.Reload() + + conf := warewulfconf.Get() + daemonPort := conf.Warewulf.Port + + auth := warewulfconf.NewAuthentication() + if util.IsFile(conf.Paths.AuthenticationConf()) { + if err := auth.Read(conf.Paths.AuthenticationConf()); err != nil { + wwlog.Warn("%w\n", err) + } + } + + apiHandler := api.Handler(auth, conf.API.AllowedIPNets()) + defaultHandler := defaultHandler() + dispatchHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/api") && conf.API != nil && conf.API.Enabled() { + apiHandler.ServeHTTP(w, r) + } else { + defaultHandler.ServeHTTP(w, r) + } + }) + if err := http.ListenAndServe(":"+strconv.Itoa(daemonPort), dispatchHandler); err != nil { + return fmt.Errorf("could not start listening service: %w", err) + } + + return nil +} diff --git a/internal/pkg/warewulfd/warewulfd.go b/internal/pkg/warewulfd/warewulfd.go deleted file mode 100644 index 152611f7..00000000 --- a/internal/pkg/warewulfd/warewulfd.go +++ /dev/null @@ -1,89 +0,0 @@ -package warewulfd - -import ( - "fmt" - "net/http" - "os" - "os/signal" - "strconv" - "strings" - "syscall" - - warewulfconf "github.com/warewulf/warewulf/internal/pkg/config" - "github.com/warewulf/warewulf/internal/pkg/wwlog" -) - -// TODO: https://github.com/danderson/netboot/blob/master/pixiecore/dhcp.go -// TODO: https://github.com/pin/tftp -/* -wrapper type for the server mux as shim requests http://efiboot//grub.efi -which is filtered out by http to `301 Moved Permanently` what -shim.efi can't handle. So filter out `//` before they hit go/http. -Makes go/http more to behave like apache -*/ -type slashFix struct { - mux http.Handler -} - -/* -Filter out the '//' -*/ -func (h *slashFix) ServeHTTP(w http.ResponseWriter, r *http.Request) { - r.URL.Path = strings.Replace(r.URL.Path, "//", "/", -1) - h.mux.ServeHTTP(w, r) -} - -func RunServer() error { - c := make(chan os.Signal, 1) - signal.Notify(c, syscall.SIGHUP) - - go func() { - for range c { - wwlog.Warn("Received SIGHUP, reloading...") - err := LoadNodeDB() - if err != nil { - wwlog.Error("Could not load node DB: %s", err) - } - - err = LoadNodeStatus() - if err != nil { - wwlog.Error("Could not prepopulate node status DB: %s", err) - } - } - }() - - err := LoadNodeDB() - if err != nil { - wwlog.Error("Could not load database: %s", err) - } - - err = LoadNodeStatus() - if err != nil { - wwlog.Error("Could not prepopulate node status DB: %s", err) - } - - if err != nil { - wwlog.Warn("couldn't copy default shim: %s", err) - } - var wwHandler http.ServeMux - wwHandler.HandleFunc("/provision/", ProvisionSend) - wwHandler.HandleFunc("/ipxe/", ProvisionSend) - wwHandler.HandleFunc("/efiboot/", ProvisionSend) - wwHandler.HandleFunc("/kernel/", ProvisionSend) - wwHandler.HandleFunc("/image/", ProvisionSend) - wwHandler.HandleFunc("/overlay-system/", ProvisionSend) - wwHandler.HandleFunc("/overlay-runtime/", ProvisionSend) - wwHandler.HandleFunc("/overlay-file/", OverlaySend) - wwHandler.HandleFunc("/status", StatusSend) - - conf := warewulfconf.Get() - - daemonPort := conf.Warewulf.Port - err = http.ListenAndServe(":"+strconv.Itoa(daemonPort), &slashFix{&wwHandler}) - - if err != nil { - return fmt.Errorf("could not start listening service: %w", err) - } - - return nil -} diff --git a/internal/pkg/wwtype/constants.go b/internal/pkg/wwtype/constants.go deleted file mode 100644 index 521f9f97..00000000 --- a/internal/pkg/wwtype/constants.go +++ /dev/null @@ -1,15 +0,0 @@ -package wwtype - -import ( - "strings" - - "github.com/warewulf/warewulf/internal/pkg/util" -) - -func GetUnsetVerbs() []string { - return []string{"unset", "delete", "undef", "--", "nil", "0.0.0.0"} -} - -func IsUnsetVerb(value string) bool { - return util.InSlice(GetUnsetVerbs(), strings.ToLower(value)) -} diff --git a/internal/pkg/wwtype/wwbool.go b/internal/pkg/wwtype/wwbool.go index bd9d65a4..c47efbb1 100644 --- a/internal/pkg/wwtype/wwbool.go +++ b/internal/pkg/wwtype/wwbool.go @@ -4,6 +4,8 @@ import ( "fmt" "strconv" "strings" + + "github.com/warewulf/warewulf/internal/pkg/util" ) // Simple string which can be converted to bool. Backend storage @@ -15,7 +17,7 @@ Transform the underlying string value to bool */ func (val WWbool) Bool() bool { str := strings.ToLower(string(val)) - if IsUnsetVerb(str) { + if isUnsetValue(str) { return false } switch str { @@ -33,7 +35,7 @@ func (val WWbool) Bool() bool { func (val WWbool) BoolDefaultTrue() bool { str := strings.ToLower(string(val)) - if IsUnsetVerb(str) { + if isUnsetValue(str) { return false } switch str { @@ -53,7 +55,7 @@ func (val WWbool) BoolDefaultTrue() bool { Set the string, only accept bool values like true, false, but also UNDEF */ func (val *WWbool) Set(str string) error { - if IsUnsetVerb(str) { + if isUnsetValue(str) { // run the unset verb trough, will be filtered out later *val = WWbool(str) return nil @@ -81,3 +83,9 @@ func (val WWbool) String() string { func (b WWbool) Type() string { return "WWbool" } + +var unsetValues = []string{"unset", "delete", "undef", "--", "nil", "0.0.0.0"} + +func isUnsetValue(value string) bool { + return util.InSlice(unsetValues, strings.ToLower(value)) +} diff --git a/overlays/wwclient/internal/wwclient_test.go b/overlays/wwclient/internal/wwclient_test.go index 328a7634..34871d2b 100644 --- a/overlays/wwclient/internal/wwclient_test.go +++ b/overlays/wwclient/internal/wwclient_test.go @@ -57,5 +57,6 @@ Filename: warewulf/init.d/80-wwclient # Only start if the systemd is not available test -e /usr/lib/systemd/systemd && exit 0 echo "Starting wwclient" +if [ -r /etc/default/wwclient ]; then . /etc/default/wwclient; fi nohup /warewulf/wwclient >/var/log/wwclient.log 2>&1 /var/log/wwclient.log 2>&1 /dev/null 2>&1 || ( lan_info="$(ipmitool lan print 1)" +if [ -n "$WWIPMI_VLAN" ]; then + prev_vlan=$(echo "$lan_info" | grep "^802.1q VLAN ID *:" | awk -F': ' '{print $2 }') + if [ "$prev_vlan" == "$WWIPMI_VLAN" ] || [ "$prev_vlan" = "Disabled" -a "$WWIPMI_VLAN" = off ]; then + echo "IPMI VLAN: $WWIPMI_VLAN" + else + echo "IPMI VLAN: $prev_vlan -> $WWIPMI_VLAN" + echo ipmitool lan set 1 vlan id "$WWIPMI_VLAN" + fi +fi + if [ -n "$WWIPMI_IPADDR" ]; then prev_ip=$(echo "$lan_info" | grep "^IP Address *:" | awk -F': ' '{ print $2 }') if [ "$prev_ip" != "$WWIPMI_IPADDR" ]; then diff --git a/overlays/wwinit/rootfs/warewulf/wwprescripts b/overlays/wwinit/rootfs/warewulf/wwprescripts index 92342b3a..ce472025 100755 --- a/overlays/wwinit/rootfs/warewulf/wwprescripts +++ b/overlays/wwinit/rootfs/warewulf/wwprescripts @@ -9,6 +9,28 @@ ls -1 "${scriptdir}/" | while read -r name; do echo "Running prescript: ${name}..." sh "${scriptdir}/${name}" done -echo -echo "Running ${WWINIT}..." -exec $WWINIT + +init="${WWINIT}" +if [ -z "${init}" ] +then + for candidate in /sbin/init /etc/init /bin/init + do + if [ -x "${candidate}" ] + then + init="${candidate}" + break + fi + done +fi + +if [ -n "${init}" ] +then + echo + echo "Running ${init}..." + exec "${init}" +else + echo "ERROR: init not defined and not found." + echo "Rebooting in 1 minute..." + sleep 60 + echo b > /proc/sysrq-trigger || /sbin/reboot -f +fi diff --git a/userdocs/getting-started/debian-quickstart.rst b/userdocs/getting-started/debian-quickstart.rst index 145ea239..81009ae4 100644 --- a/userdocs/getting-started/debian-quickstart.rst +++ b/userdocs/getting-started/debian-quickstart.rst @@ -31,7 +31,7 @@ Install Warewulf and dependencies cd ~/git git clone https://github.com/warewulf/warewulf.git cd warewulf - git checkout main # or switch to a tag like 'v4.6.0' + git checkout main # or switch to a tag like 'v4.6.1' make all && sudo make install Configure firewalld diff --git a/userdocs/getting-started/el-quickstart.rst b/userdocs/getting-started/el-quickstart.rst index efb2eb2d..4e701b8a 100644 --- a/userdocs/getting-started/el-quickstart.rst +++ b/userdocs/getting-started/el-quickstart.rst @@ -10,11 +10,11 @@ Install Warewulf The preferred way to install Warewulf on Enterprise Linux is using the the RPMs published in `GitHub releases`_. For example, to install the -v4.6.0 release on Enterprise Linux 9: +v4.6.1 release on Enterprise Linux 9: .. code-block:: bash - dnf install https://github.com/warewulf/warewulf/releases/download/v4.6.0/warewulf-4.6.0-1.el9.x86_64.rpm + dnf install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-4.6.1-1.el9.x86_64.rpm Packages are available for el8 and el9. diff --git a/userdocs/images/images.rst b/userdocs/images/images.rst index 918d7db4..3cdab56d 100644 --- a/userdocs/images/images.rst +++ b/userdocs/images/images.rst @@ -227,8 +227,8 @@ be rebuilt into a bootable static object automatically. (To skip the automatic image rebuild, specify ``--build=false``.) If the files ``/etc/passwd`` or ``/etc/group`` were updated, there will be an -additional check to confirm if the users are in sync as described in `Syncuser`_ -section. +additional check to confirm if the users are in sync as described in the +:ref:`Syncuser` section. Specifying a Prompt ------------------- diff --git a/userdocs/index.rst b/userdocs/index.rst index 9e15ad27..23a04c2d 100644 --- a/userdocs/index.rst +++ b/userdocs/index.rst @@ -27,6 +27,7 @@ Welcome to the Warewulf User Guide! Security Bootloaders Upgrading Warewulf + REST API .. toctree:: :maxdepth: 1 @@ -89,3 +90,4 @@ Welcome to the Warewulf User Guide! :caption: Release Notes v4.6.0 + v4.6.1 diff --git a/userdocs/nodes/ipmi.rst b/userdocs/nodes/ipmi.rst index 4e198d54..b8384aa6 100644 --- a/userdocs/nodes/ipmi.rst +++ b/userdocs/nodes/ipmi.rst @@ -28,6 +28,13 @@ desired IPMI configuration to the node's BMC during boot. wwctl node set n1 \ --ipmiaddr=192.168.2.1 +Additionally, a ``vlan`` ipmi tag can be used to set the IPMI VLAN ID. + +.. code-block:: + + wwctl profile set default \ + --ipmitagadd vlan=42 + ``wwctl node list`` has a specific overview for IPMI settings. .. code-block:: console diff --git a/userdocs/nodes/network.rst b/userdocs/nodes/network.rst index e62a3898..2eee1107 100644 --- a/userdocs/nodes/network.rst +++ b/userdocs/nodes/network.rst @@ -51,16 +51,16 @@ The ``ifcfg`` and ``NetworkManager`` overlays can configure a network bond like device: bond0 ipaddr: 192.168.3.100 netmask: 255.255.255.0 - en1: - device: en1 - hwaddr: e6:92:39:49:7b:03 - tags: - master: bond0 - en2: - device: en2 - hwaddr: 9a:77:29:73:14:f1 - tags: - master: bond0 + en1: + device: en1 + hwaddr: e6:92:39:49:7b:03 + tags: + master: bond0 + en2: + device: en2 + hwaddr: 9a:77:29:73:14:f1 + tags: + master: bond0 .. _vlan: diff --git a/userdocs/overlays/overlays.rst b/userdocs/overlays/overlays.rst index eee44bbc..28222128 100644 --- a/userdocs/overlays/overlays.rst +++ b/userdocs/overlays/overlays.rst @@ -52,7 +52,7 @@ A node or profile can configure an overlay in two different ways: .. code-block:: shell wwctl profile set default \ - --system-overlays="wwinit,,wwclient,fstab,hostname,ssh.host_keys,systemd.netname,NetworkManager" \ + --system-overlays="wwinit,wwclient,fstab,hostname,ssh.host_keys,systemd.netname,NetworkManager" \ --runime-overlays="hosts,ssh.authorized_keys" Multiple overlays can be applied to a single node, and overlays from multiple @@ -202,6 +202,10 @@ All configured overlays are provisioned initially along with the node image itself; but **wwclient** periodically fetches and applies the runtime overlay to allow configuration of some settings without a reboot. +wwclient will contat the ``ipaddr`` value from ``warewulf.conf`` by default. +This can be overridden by specifying a ``WW_IPADDR`` environment variable, which +can be set via an overlay in ``/etc/default/wwclient``. + Network interfaces ------------------ @@ -280,6 +284,8 @@ Two SSH overlays configure host keys (one set for all node in the cluster) and - ssh.authorized_keys - ssh.host_keys +.. _Syncuser: + syncuser -------- @@ -294,6 +300,11 @@ If a ``PasswordlessRoot`` tag is set to "true", the overlay will also insert a "passwordless" root entry. This can be particularly useful for accessing a cluster node when its network interface is not properly configured. +.. warning:: + + ``PasswordlessRoot`` is not recommended for production; it should only be + used during debugging, when normal authentication is not functional. + ignition -------- diff --git a/userdocs/release/v4.6.1.rst b/userdocs/release/v4.6.1.rst new file mode 100644 index 00000000..450af0e1 --- /dev/null +++ b/userdocs/release/v4.6.1.rst @@ -0,0 +1,122 @@ +==================== +v4.6.1 Release Notes +==================== + +v4.6.1 is a regularly-scheduled minor release in the v4.6.x series. + +Significant changes are described below. Additional changes are listed in the +`CHANGELOG`_. + +.. _CHANGELOG: https://github.com/warewulf/warewulf/blob/main/CHANGELOG.md + +aarch64 packages +================ + +We're now building and publishing aarch64 packages with GitHub releases. + +New REST API +============ + +Possibly the largest change in v4.6.1 is the addition of a new :ref:`REST API +`. This API is optionally served by ``warewulfd`` at ``/api``, and is +disabled by default. + +Changed JSON output +=================== + +The REST API returns JSON, which has brought additional attention to the JSON +output previously only output by ``wwctl list --json``. This +output has been updated such that the JSON object properties match field names +used in ``nodes.conf``. JSON output also now omits empty or unspecified values. + +New command-line options +======================== + +A couple new ``wwctl`` options have been added: + +- ``wwctl overlay import --overwrite`` overwrites existing overlay files during + import. +- ``wwctl node import --yes`` skips the interactive confirmaton dialog and + assumes a "yes" answer. + +New tags +======== + +A new ``vlan`` IPMI tag configures the IPMI interface to use the specified vlan +during ``ipmiwrite``. + +.. code-block:: + + wwctl profile set default --ipmitagadd=vlan=100 + +Customize wwclient connection to Warewulf server +================================================ + +In some circumstances (e.g., when isolating compute nodes from the provisioning +network) it may be useful for ``wwclient`` to connect to a different IP address +than the default Warewulf server address. To support this, ``wwclient`` will +connect to the address specified in the environment variable ``WW_IPADDR``, if +present. + +This variable can be specified in ``/etc/default/wwclient`` with an overlay. + +Distribution-specific fixes +=========================== + +Downstream SUSE packages have historically set a kernel argument +``net.ifnames=1`` to `enable predictable network interfaces`_. This argument is +now set in the initial ``nodes.conf`` and during ``wwctl upgrade nodes +--add-defaults``. + +.. _enable predictable network interfaces: https://documentation.suse.com/smart/network/html/network-interface-predictable-naming/index.html + +Upgrade fixes and default behaviors +=================================== + +v4.6.0 moved default settings from ``defaults.conf`` (and, before that, +compiled-in constants) with settings on the "default" profile in the initial +``nodes.conf``, and added ``wwctl upgrade nodes --add-defaults`` to add default +settings to an existing ``nodes.conf``. However, this removal of default +settings caused surprising deficiencies in some cases; and, in others +``--add-defaults`` simply failed to set proper defaults. + +- ``wwctl upgrade nodes --add-defaults`` now sets a default iPXE template. If no + default template is set, ``warewulfd`` looks for a template named "default". +- If no init is specified, ``wwinit`` now looks for ``/sbin/init``, + ``/etc/init``, and ``/bin/init``. +- `wwctl upgrade nodes --replace-overlays` now avoids adding the same overlay + multiple times to the same node or profile. +- If, during ``wwctl overlay build``, a node has an empty system or runtime + overlay, a warning is printed. + +New SOS plugin +============== + +A new ``warewulf-sos`` subpackage installs an SOS plugin to gather logs and +other state from the Warewulf server for filing a support request. Currently +only available in Enterprise Linux distributions. + +.. code-block:: + + sos report --enable-plugins warewulf + +Misc. bug fixes +=============== + +- Fixed a panic in ``warewulfd`` when a network device isn't configured + explicitly on a node. +- ``wwctl `` now better handles missing + required IPMI fields. +- Field names containing periods (e.g., ``NetDev[eth0.100]``) are now displayed + properly in ``wwctl list``. +- Properly quote a specified escape character during ``ipmitool`` commands. +- Update related nodes and profiles when renaming an image. +- ``wwctl set`` now properly handles "UNDEF" and "UNSET" to + remove a defined field value. +- Configure the GRUB bootloader to sleep and reboot on certain errors (to allow + error messages to be read). +- ``wwctl node import`` can now import new nodes, not just update existing + nodes. +- Fixed a panic during ``wwctl node list --ipmi`` for nodes with no IPMI + configuration. +- Fixed processing of ``--verbose`` and ``--debug`` when starting ``warewulfd``. diff --git a/userdocs/server/api.rst b/userdocs/server/api.rst new file mode 100644 index 00000000..4b9053b4 --- /dev/null +++ b/userdocs/server/api.rst @@ -0,0 +1,77 @@ +.. _rest-api: + +======== +REST API +======== + +On-line documentation for the API is available at ``/api/docs``. + +Authentication +============== + +Authentication is managed at ``/etc/warewulf/auth.conf``. This is a +YAML formatted file with a single key: ``users:``, that is a list of user names +and passwords able to authenticate to the API. + +.. warning:: + + Because ``warewulfd`` runs as ``root`` by default, and because ``warewulfd`` + can run effectively arbitrary code via overlay templates, API access is + tantamount to ``root`` access on the Warewulf server. For this reason, the + API is only accessible via localhost by default. Still, handle API + credentials with care. + +.. code-block:: yaml + + users: + - name: admin + password hash: $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC + +Passwords are stored as bcrypt2 hashses, which can be generated with ``mkpasswd``. + +.. code-block:: console + + $ mkpasswd --method=bcrypt + Password: # admin + $2b$05$5QVWDpiWE7L4SDL9CYdi3O/l6HnbNOLoXgY2sa1bQQ7aSBKdSqvsC + +Node +==== + +* ``GET /api/nodes/``: Get nodes +* ``POST /api/nodes/overlays/build``: Build all overlays +* ``DELETE /api/nodes/{id}``: Delete an existing node +* ``GET /api/nodes/{id}``: Get a node +* ``PATCH /api/nodes/{id}``: Update an existing node +* ``PUT /api/nodes/{id}``: Add a node +* ``GET /api/nodes/{id}/fields``: Get node fields +* ``POST /api/nodes/{id}/overlays/build``: Build overlays for a node +* ``GET /api/nodes/{id}/raw``: Get a raw node + +Profile +======= + +* ``GET /api/profiles/``: Get node profiles +* ``DELETE /api/profiles/{id}``: Delete an existing profile +* ``GET /api/profiles/{id}``: Get a node profile +* ``PATCH /api/profiles/{id}``: Update an existing profile +* ``PUT /api/profiles/{id}``: Add a profile + +Image +===== + +* ``GET /api/images``: Get all images +* ``DELETE /api/images/{name}``: Delete an image +* ``GET /api/images/{name}``: Get an image +* ``PATCH /api/images/{name}``: Update or rename an image +* ``POST /api/images/{name}/build``: Build an image +* ``POST /api/images/{name}/import``: Import an image + +Overlay +======= + +* ``GET /api/overlays/``: Get overlays +* ``DELETE /api/overlays/{name}``: Delete an overlay +* ``GET /api/overlays/{name}``: Get an overlay +* ``PUT /api/overlays/{name}``: Create an overlay +* ``GET /api/overlays/{name}/file``: Get an overlay file diff --git a/userdocs/server/bootloaders.rst b/userdocs/server/bootloaders.rst index 9132e1ec..61b0b6a3 100644 --- a/userdocs/server/bootloaders.rst +++ b/userdocs/server/bootloaders.rst @@ -334,13 +334,13 @@ Warewulf provides a dracut module to configure the dracut initramfs to load the image. This module is available in the ``warewulf-dracut`` subpackage, which must be installed in the image. -With the ``warewulf-dracut`` package installed, you can build an initramfs -inside the image. +With the ``warewulf-dracut`` package installed in the image, you can then build +an initramfs inside the image. .. code-block:: shell - dnf -y install warewulf-dracut - dracut --force --no-hostonly --add wwinit --regenerate-all + wwctl image exec rockylinux-9 --build=false -- /usr/bin/dnf -y install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-dracut-4.6.1-1.el9.noarch.rpm + wwctl image exec rockylinux-9 -- /usr/bin/dracut --force --no-hostonly --add wwinit --regenerate-all .. note:: diff --git a/userdocs/server/configuration.rst b/userdocs/server/configuration.rst index 43248d92..df7f6e65 100644 --- a/userdocs/server/configuration.rst +++ b/userdocs/server/configuration.rst @@ -333,6 +333,26 @@ Configuration for the ``wwclient`` service on cluster nodes. ``wwclient`` will use the TCP port "987" by default if ``secure: true``; but, if that port is otherwise in use, a different port may be specified. +api +=== + +*New in Warewulf v4.6.1* + +Configuration for the REST API of the ``warewulfd`` service. + +.. code-block:: yaml + + api: + enabled: true + allowed subnets: + - 127.0.0.0/8 + - ::1/128 + +* ``api:enabled``: Whether the ``warewulfd`` service should provide access via a + REST interface. +* ``api:allowed subnets``: Which subnets are allowed to access the REST API. By + default, only localhost has access. + hostfile ======== diff --git a/userdocs/server/installation.rst b/userdocs/server/installation.rst index 986ab145..a01e5f0f 100644 --- a/userdocs/server/installation.rst +++ b/userdocs/server/installation.rst @@ -18,14 +18,14 @@ Rocky Linux 9 .. code-block:: console - # dnf install https://github.com/warewulf/warewulf/releases/download/v4.6.0rc2/warewulf-4.6.0rc2-1.el9.x86_64.rpm + # dnf install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-4.6.1-1.el9.x86_64.rpm openSuse Leap ------------- .. code-block:: console - # zypper install https://github.com/warewulf/warewulf/releases/download/v4.6.0rc2/warewulf-4.6.0rc2-1.suse.lp155.x86_64.rpm + # zypper install https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-4.6.1-1.suse.lp155.x86_64.rpm Container images ================ @@ -97,9 +97,9 @@ appropriate substitutions: .. code-block:: bash - curl -LO https://github.com/warewulf/warewulf/releases/download/v4.6.0rc2/warewulf-4.6.0rc2.tar.gz - tar -xf warewulf-4.6.0rc2.tar.gz - cd warewulf-4.6.0rc2 + curl -LO https://github.com/warewulf/warewulf/releases/download/v4.6.1/warewulf-4.6.1.tar.gz + tar -xf warewulf-4.6.1.tar.gz + cd warewulf-4.6.1 make all && sudo make install Git @@ -118,7 +118,7 @@ from the main site, the GitHub releases page, or from a Git tag. git clone https://github.com/warewulf/warewulf.git cd warewulf - git checkout main # or switch to a tag like 'v4.6.0rc2' + git checkout main # or switch to a tag like 'v4.6.1' make all && sudo make install Runtime Dependencies @@ -134,6 +134,22 @@ services. Generally these are provided by your distribution. If you are using an Enterprise Linux compatible distribution you can install them with ``dnf install dhcp-server tftp-server nfs-utils``. +Building RPM packages from source +================================= + +You can also build RPM packages from source. + +.. code-block:: bash + + dnf -y install epel-release + dnf -y install make mock + git clone git@github.com:warewulf/warewulf.git + ( + cd warewulf + make clean && make dist warewulf.spec && mock -r rocky+epel-9-$(arch) --rebuild --spec=warewulf.spec --sources=. + ) + dnf -y install /var/lib/mock/rocky+epel-9-$(arch)/result/warewulf-*.$(arch).rpm + Starting warewulfd ================== diff --git a/warewulf.spec.in b/warewulf.spec.in index 5cebdd3e..ddc0e313 100644 --- a/warewulf.spec.in +++ b/warewulf.spec.in @@ -1,7 +1,5 @@ %global debug_package %{nil} -%global api 0 - %if 0%{?suse_version} %global tftpdir /srv/tftpboot %else @@ -73,9 +71,6 @@ BuildRequires: git BuildRequires: make BuildRequires: gpgme-devel BuildRequires: python3-devel -%if %{api} -BuildRequires: libassuan-devel -%endif Recommends: logrotate Recommends: ipmitool @@ -113,9 +108,6 @@ make defaults \ SOSPLUGINS=%{python3_sitelib}/sos/report/plugins \ CACHEDIR=%{_localstatedir}/cache make build -%if %{api} -make api -%endif %install @@ -123,10 +115,6 @@ export OFFLINE_BUILD=1 export NO_BRP_STALE_LINK_ERROR=yes make install \ DESTDIR=%{buildroot} -%if %{api} -make installapi \ - DESTDIR=%{buildroot} -%endif %if 0%{?suse_version} || 0%{?sle_version} yq e ' @@ -135,6 +123,9 @@ yq e ' .tftp.ipxe."00:09" = "ipxe-x86_64.efi" | .tftp.ipxe."00:0B" = "snp-arm64.efi" ' \ -i %{buildroot}%{_sysconfdir}/warewulf/warewulf.conf +%else +make install-sos \ + DESTDIR=%{buildroot} %endif %pre @@ -162,6 +153,7 @@ getent group %{wwgroup} >/dev/null || groupadd -r %{wwgroup} %dir %{_sysconfdir}/warewulf %config(noreplace) %{_sysconfdir}/warewulf/warewulf.conf %config(noreplace) %attr(0640,-,%{wwgroup}) %{_sysconfdir}/warewulf/nodes.conf +%config(noreplace) %attr(0600,-,-) %{_sysconfdir}/warewulf/auth.conf %config(noreplace) %{_sysconfdir}/warewulf/examples %config(noreplace) %{_sysconfdir}/warewulf/ipxe %config(noreplace) %{_sysconfdir}/warewulf/grub @@ -208,11 +200,6 @@ getent group %{wwgroup} >/dev/null || groupadd -r %{wwgroup} %dir %{_docdir}/warewulf %license %{_docdir}/warewulf/LICENSE.md -%if %{api} -%{_bindir}/wwapi* -%config(noreplace) %{_sysconfdir}/warewulf/wwapi*.conf -%endif - %package dracut Summary: dracut module for loading a Warewulf image @@ -260,6 +247,9 @@ about Warewulf in an sos report. %changelog +* Tue Apr 1 2025 Jonathon Anderson +- Remove gRPC API + * Thu Feb 20 2025 Stephen Simpson - Added sos plugin