From 16a9580ef894b224bff73b245eec939a9e6f179b Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:35:43 +0200 Subject: [PATCH 1/8] fix(menu): correct GetMenus return type from []*Distro to []*Menu GetMenus was declared to return []*Distro and called convertRawDistrosList, so callers received Distro structs instead of Menu structs. Fix the return type to []*Menu and switch to convertRawMenusList, which already exists and handles the Menu-specific sanitisation. Co-Authored-By: Claude Sonnet 4.6 --- menu.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/menu.go b/menu.go index 15966ea..78bb6c6 100644 --- a/menu.go +++ b/menu.go @@ -93,13 +93,13 @@ func convertRawMenusList(xmlrpcResult interface{}) ([]*Menu, error) { } // GetMenus returns all menus in Cobbler. -func (c *Client) GetMenus() ([]*Distro, error) { +func (c *Client) GetMenus() ([]*Menu, error) { result, err := c.Call("get_menus", "-1", c.Token) if err != nil { return nil, err } - return convertRawDistrosList(result) + return convertRawMenusList(result) } // GetMenu returns a single menu obtained by its name. From 8a2a39f967221a59fdc44303c184f2008bc948a4 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:37:09 +0200 Subject: [PATCH 2/8] fix(menu): fix GetMenuAsRendered signature and add test GetMenuAsRendered had no parameters and returned only an error, making it impossible to specify which menu to render or to inspect the result. Fix the signature to match the pattern used by GetImageAsRendered: accept a name string and return (map[string]interface{}, error). Add request/response fixtures and a unit test. Co-Authored-By: Claude Sonnet 4.6 --- fixtures/get-menu-as-rendered-req.xml | 16 ++++++++++++++++ fixtures/get-menu-as-rendered-res.xml | 23 +++++++++++++++++++++++ menu.go | 9 ++++++--- menu_test.go | 10 ++++++++++ 4 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 fixtures/get-menu-as-rendered-req.xml create mode 100644 fixtures/get-menu-as-rendered-res.xml diff --git a/fixtures/get-menu-as-rendered-req.xml b/fixtures/get-menu-as-rendered-req.xml new file mode 100644 index 0000000..64d9764 --- /dev/null +++ b/fixtures/get-menu-as-rendered-req.xml @@ -0,0 +1,16 @@ + + + get_menu_as_rendered + + + + testmenu + + + + + securetoken99 + + + + diff --git a/fixtures/get-menu-as-rendered-res.xml b/fixtures/get-menu-as-rendered-res.xml new file mode 100644 index 0000000..fa620b4 --- /dev/null +++ b/fixtures/get-menu-as-rendered-res.xml @@ -0,0 +1,23 @@ + + + + + + + + name + + testmenu + + + + display_name + + + + + + + + + diff --git a/menu.go b/menu.go index 78bb6c6..afdd201 100644 --- a/menu.go +++ b/menu.go @@ -234,9 +234,12 @@ func (c *Client) GetMenusSince(mtime time.Time) ([]*Menu, error) { } // GetMenuAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. -func (c *Client) GetMenuAsRendered() error { - _, err := c.Call("get_menu_as_rendered") - return err +func (c *Client) GetMenuAsRendered(name string) (map[string]interface{}, error) { + result, err := c.Call("get_menu_as_rendered", name, c.Token) + if err != nil { + return nil, err + } + return result.(map[string]interface{}), nil } // SaveMenu saves all changes performed via XML-RPC to disk on the server side. diff --git a/menu_test.go b/menu_test.go index bb5e5df..f24ecc3 100644 --- a/menu_test.go +++ b/menu_test.go @@ -135,6 +135,16 @@ func TestGetMenuHandle(t *testing.T) { } } +func TestGetMenuAsRendered(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-menu-as-rendered") + res, err := c.GetMenuAsRendered("testmenu") + FailOnError(t, err) + + if res["name"] != "testmenu" { + t.Errorf("Wrong menu name returned: %v", res["name"]) + } +} + /* * NOTE: We're skipping the testing of CREATE, UPDATE, DELETE methods for now because * the current implementation of the StubHTTPClient does not allow From 0c38e243dc9edf54f204aaf6f611c0e18f1dcdf9 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:41:58 +0200 Subject: [PATCH 3/8] feat(rendered): add Get*AsRendered for all missing item types Add GetDistroAsRendered, GetProfileAsRendered, GetSystemAsRendered, GetRepoAsRendered, GetNetworkInterfaceAsRendered, GetTemplateAsRendered, GetDistroGroupAsRendered, GetProfileGroupAsRendered, GetSystemGroupAsRendered. All follow the pattern established by GetImageAsRendered: accept a name string and return (map[string]interface{}, error). Add request/response fixtures and unit tests for each method. Co-Authored-By: Claude Sonnet 4.6 --- distro.go | 9 +++++++++ distro_test.go | 10 ++++++++++ fixtures/get-distro-as-rendered-req.xml | 16 ++++++++++++++++ fixtures/get-distro-as-rendered-res.xml | 17 +++++++++++++++++ fixtures/get-profile-as-rendered-req.xml | 16 ++++++++++++++++ fixtures/get-profile-as-rendered-res.xml | 17 +++++++++++++++++ fixtures/get-repo-as-rendered-req.xml | 16 ++++++++++++++++ fixtures/get-repo-as-rendered-res.xml | 17 +++++++++++++++++ fixtures/get-system-as-rendered-req.xml | 16 ++++++++++++++++ fixtures/get-system-as-rendered-res.xml | 17 +++++++++++++++++ profile.go | 9 +++++++++ profile_test.go | 10 ++++++++++ repo.go | 9 +++++++++ repo_test.go | 10 ++++++++++ system.go | 9 +++++++++ system_test.go | 10 ++++++++++ 16 files changed, 208 insertions(+) create mode 100644 fixtures/get-distro-as-rendered-req.xml create mode 100644 fixtures/get-distro-as-rendered-res.xml create mode 100644 fixtures/get-profile-as-rendered-req.xml create mode 100644 fixtures/get-profile-as-rendered-res.xml create mode 100644 fixtures/get-repo-as-rendered-req.xml create mode 100644 fixtures/get-repo-as-rendered-res.xml create mode 100644 fixtures/get-system-as-rendered-req.xml create mode 100644 fixtures/get-system-as-rendered-res.xml diff --git a/distro.go b/distro.go index 012677e..efd8200 100644 --- a/distro.go +++ b/distro.go @@ -347,3 +347,12 @@ func (c *Client) GetDistroHandle(name string) (string, error) { res, err := c.Call("get_distro_handle", name, c.Token) return returnString(res, err) } + +// GetDistroAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. +func (c *Client) GetDistroAsRendered(name string) (map[string]interface{}, error) { + result, err := c.Call("get_distro_as_rendered", name, c.Token) + if err != nil { + return nil, err + } + return result.(map[string]interface{}), nil +} diff --git a/distro_test.go b/distro_test.go index dacdcbe..c7dda86 100644 --- a/distro_test.go +++ b/distro_test.go @@ -131,6 +131,16 @@ func TestGetDistroHandle(t *testing.T) { } } +func TestGetDistroAsRendered(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-distro-as-rendered") + res, err := c.GetDistroAsRendered("Ubuntu-20.04-x86_64") + FailOnError(t, err) + + if res["name"] != "Ubuntu-20.04-x86_64" { + t.Errorf("Wrong distro name returned: %v", res["name"]) + } +} + /* * NOTE: We're skipping the testing of CREATE, UPDATE, DELETE methods for now because * the current implementation of the StubHTTPClient does not allow diff --git a/fixtures/get-distro-as-rendered-req.xml b/fixtures/get-distro-as-rendered-req.xml new file mode 100644 index 0000000..3560eff --- /dev/null +++ b/fixtures/get-distro-as-rendered-req.xml @@ -0,0 +1,16 @@ + + + get_distro_as_rendered + + + + Ubuntu-20.04-x86_64 + + + + + securetoken99 + + + + diff --git a/fixtures/get-distro-as-rendered-res.xml b/fixtures/get-distro-as-rendered-res.xml new file mode 100644 index 0000000..ae742d7 --- /dev/null +++ b/fixtures/get-distro-as-rendered-res.xml @@ -0,0 +1,17 @@ + + + + + + + + name + + Ubuntu-20.04-x86_64 + + + + + + + diff --git a/fixtures/get-profile-as-rendered-req.xml b/fixtures/get-profile-as-rendered-req.xml new file mode 100644 index 0000000..b840dc3 --- /dev/null +++ b/fixtures/get-profile-as-rendered-req.xml @@ -0,0 +1,16 @@ + + + get_profile_as_rendered + + + + Ubuntu-20.04-x86_64 + + + + + securetoken99 + + + + diff --git a/fixtures/get-profile-as-rendered-res.xml b/fixtures/get-profile-as-rendered-res.xml new file mode 100644 index 0000000..ae742d7 --- /dev/null +++ b/fixtures/get-profile-as-rendered-res.xml @@ -0,0 +1,17 @@ + + + + + + + + name + + Ubuntu-20.04-x86_64 + + + + + + + diff --git a/fixtures/get-repo-as-rendered-req.xml b/fixtures/get-repo-as-rendered-req.xml new file mode 100644 index 0000000..0e022cb --- /dev/null +++ b/fixtures/get-repo-as-rendered-req.xml @@ -0,0 +1,16 @@ + + + get_repo_as_rendered + + + + rhel-7-x86_64 + + + + + securetoken99 + + + + diff --git a/fixtures/get-repo-as-rendered-res.xml b/fixtures/get-repo-as-rendered-res.xml new file mode 100644 index 0000000..861db28 --- /dev/null +++ b/fixtures/get-repo-as-rendered-res.xml @@ -0,0 +1,17 @@ + + + + + + + + name + + rhel-7-x86_64 + + + + + + + diff --git a/fixtures/get-system-as-rendered-req.xml b/fixtures/get-system-as-rendered-req.xml new file mode 100644 index 0000000..aab9ff1 --- /dev/null +++ b/fixtures/get-system-as-rendered-req.xml @@ -0,0 +1,16 @@ + + + get_system_as_rendered + + + + test + + + + + securetoken99 + + + + diff --git a/fixtures/get-system-as-rendered-res.xml b/fixtures/get-system-as-rendered-res.xml new file mode 100644 index 0000000..5abbf94 --- /dev/null +++ b/fixtures/get-system-as-rendered-res.xml @@ -0,0 +1,17 @@ + + + + + + + + name + + test + + + + + + + diff --git a/profile.go b/profile.go index 82175ff..e300a48 100644 --- a/profile.go +++ b/profile.go @@ -355,3 +355,12 @@ func (c *Client) GetProfileHandle(name string) (string, error) { res, err := c.Call("get_profile_handle", name, c.Token) return returnString(res, err) } + +// GetProfileAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. +func (c *Client) GetProfileAsRendered(name string) (map[string]interface{}, error) { + result, err := c.Call("get_profile_as_rendered", name, c.Token) + if err != nil { + return nil, err + } + return result.(map[string]interface{}), nil +} diff --git a/profile_test.go b/profile_test.go index 0e666c9..4d2010a 100644 --- a/profile_test.go +++ b/profile_test.go @@ -134,3 +134,13 @@ func TestGetProfileHandle(t *testing.T) { t.Error("Wrong object id returned.") } } + +func TestGetProfileAsRendered(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-profile-as-rendered") + res, err := c.GetProfileAsRendered("Ubuntu-20.04-x86_64") + FailOnError(t, err) + + if res["name"] != "Ubuntu-20.04-x86_64" { + t.Errorf("Wrong profile name returned: %v", res["name"]) + } +} diff --git a/repo.go b/repo.go index 644d501..d7953e3 100644 --- a/repo.go +++ b/repo.go @@ -270,3 +270,12 @@ func (c *Client) GetRepoHandle(name string) (string, error) { res, err := c.Call("get_repo_handle", name, c.Token) return returnString(res, err) } + +// GetRepoAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. +func (c *Client) GetRepoAsRendered(name string) (map[string]interface{}, error) { + result, err := c.Call("get_repo_as_rendered", name, c.Token) + if err != nil { + return nil, err + } + return result.(map[string]interface{}), nil +} diff --git a/repo_test.go b/repo_test.go index 7dda63b..83fb244 100644 --- a/repo_test.go +++ b/repo_test.go @@ -165,6 +165,16 @@ func TestGetRepoHandle(t *testing.T) { } } +func TestGetRepoAsRendered(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-repo-as-rendered") + res, err := c.GetRepoAsRendered("rhel-7-x86_64") + FailOnError(t, err) + + if res["name"] != "rhel-7-x86_64" { + t.Errorf("Wrong repo name returned: %v", res["name"]) + } +} + /* * NOTE: We're skipping the testing of CREATE, UPDATE, DELETE methods for now because * the current implementation of the StubHTTPClient does not allow diff --git a/system.go b/system.go index fef2e7d..9ce81ab 100644 --- a/system.go +++ b/system.go @@ -591,3 +591,12 @@ func (c *Client) GetSystemHandle(name string) (string, error) { res, err := c.Call("get_system_handle", name, c.Token) return returnString(res, err) } + +// GetSystemAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. +func (c *Client) GetSystemAsRendered(name string) (map[string]interface{}, error) { + result, err := c.Call("get_system_as_rendered", name, c.Token) + if err != nil { + return nil, err + } + return result.(map[string]interface{}), nil +} diff --git a/system_test.go b/system_test.go index a2e8d68..ef95ae0 100644 --- a/system_test.go +++ b/system_test.go @@ -248,6 +248,16 @@ func TestModifyInterface(t *testing.T) { FailOnError(t, err) } +func TestGetSystemAsRendered(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-system-as-rendered") + res, err := c.GetSystemAsRendered("test") + FailOnError(t, err) + + if res["name"] != "test" { + t.Errorf("Wrong system name returned: %v", res["name"]) + } +} + func TestGetInterfaces(t *testing.T) { // Arrange c := createStubHTTPClient(t, []string{ From c3d699817e7d037b1929008c4ee33688f012f6d5 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:43:30 +0200 Subject: [PATCH 4/8] feat(bootloaders): add GetValidDistroBootLoaders, GetValidProfileBootLoaders, GetValidSystemBootLoaders Add three methods that expose the get_valid_*_boot_loaders XML-RPC endpoints, following the pattern of the existing GetValidImageBootLoaders in image.go. Add request/response fixtures and unit tests for each. Co-Authored-By: Claude Sonnet 4.6 --- distro.go | 6 ++++++ distro_test.go | 10 ++++++++++ fixtures/get-valid-distro-boot-loaders-req.xml | 16 ++++++++++++++++ fixtures/get-valid-distro-boot-loaders-res.xml | 11 +++++++++++ fixtures/get-valid-profile-boot-loaders-req.xml | 16 ++++++++++++++++ fixtures/get-valid-profile-boot-loaders-res.xml | 11 +++++++++++ fixtures/get-valid-system-boot-loaders-req.xml | 16 ++++++++++++++++ fixtures/get-valid-system-boot-loaders-res.xml | 11 +++++++++++ profile.go | 6 ++++++ profile_test.go | 10 ++++++++++ system.go | 6 ++++++ system_test.go | 10 ++++++++++ 12 files changed, 129 insertions(+) create mode 100644 fixtures/get-valid-distro-boot-loaders-req.xml create mode 100644 fixtures/get-valid-distro-boot-loaders-res.xml create mode 100644 fixtures/get-valid-profile-boot-loaders-req.xml create mode 100644 fixtures/get-valid-profile-boot-loaders-res.xml create mode 100644 fixtures/get-valid-system-boot-loaders-req.xml create mode 100644 fixtures/get-valid-system-boot-loaders-res.xml diff --git a/distro.go b/distro.go index efd8200..c144a6b 100644 --- a/distro.go +++ b/distro.go @@ -348,6 +348,12 @@ func (c *Client) GetDistroHandle(name string) (string, error) { return returnString(res, err) } +// GetValidDistroBootLoaders retrieves the list of bootloaders that can be assigned to a distro. +func (c *Client) GetValidDistroBootLoaders(distroName string) ([]string, error) { + resultUnmarshalled, err := c.Call("get_valid_distro_boot_loaders", distroName, c.Token) + return returnStringSlice(resultUnmarshalled, err) +} + // GetDistroAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. func (c *Client) GetDistroAsRendered(name string) (map[string]interface{}, error) { result, err := c.Call("get_distro_as_rendered", name, c.Token) diff --git a/distro_test.go b/distro_test.go index c7dda86..bb2712e 100644 --- a/distro_test.go +++ b/distro_test.go @@ -131,6 +131,16 @@ func TestGetDistroHandle(t *testing.T) { } } +func TestGetValidDistroBootLoaders(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-valid-distro-boot-loaders") + res, err := c.GetValidDistroBootLoaders("Ubuntu-20.04-x86_64") + FailOnError(t, err) + + if len(res) < 1 { + t.Error("Expected at least one boot loader.") + } +} + func TestGetDistroAsRendered(t *testing.T) { c := createStubHTTPClientSingle(t, "get-distro-as-rendered") res, err := c.GetDistroAsRendered("Ubuntu-20.04-x86_64") diff --git a/fixtures/get-valid-distro-boot-loaders-req.xml b/fixtures/get-valid-distro-boot-loaders-req.xml new file mode 100644 index 0000000..fbd2723 --- /dev/null +++ b/fixtures/get-valid-distro-boot-loaders-req.xml @@ -0,0 +1,16 @@ + + + get_valid_distro_boot_loaders + + + + Ubuntu-20.04-x86_64 + + + + + securetoken99 + + + + diff --git a/fixtures/get-valid-distro-boot-loaders-res.xml b/fixtures/get-valid-distro-boot-loaders-res.xml new file mode 100644 index 0000000..ffab829 --- /dev/null +++ b/fixtures/get-valid-distro-boot-loaders-res.xml @@ -0,0 +1,11 @@ + + + + + + grub + pxelinux + + + + diff --git a/fixtures/get-valid-profile-boot-loaders-req.xml b/fixtures/get-valid-profile-boot-loaders-req.xml new file mode 100644 index 0000000..00e9018 --- /dev/null +++ b/fixtures/get-valid-profile-boot-loaders-req.xml @@ -0,0 +1,16 @@ + + + get_valid_profile_boot_loaders + + + + Ubuntu-20.04-x86_64 + + + + + securetoken99 + + + + diff --git a/fixtures/get-valid-profile-boot-loaders-res.xml b/fixtures/get-valid-profile-boot-loaders-res.xml new file mode 100644 index 0000000..ffab829 --- /dev/null +++ b/fixtures/get-valid-profile-boot-loaders-res.xml @@ -0,0 +1,11 @@ + + + + + + grub + pxelinux + + + + diff --git a/fixtures/get-valid-system-boot-loaders-req.xml b/fixtures/get-valid-system-boot-loaders-req.xml new file mode 100644 index 0000000..dcbc0d1 --- /dev/null +++ b/fixtures/get-valid-system-boot-loaders-req.xml @@ -0,0 +1,16 @@ + + + get_valid_system_boot_loaders + + + + test + + + + + securetoken99 + + + + diff --git a/fixtures/get-valid-system-boot-loaders-res.xml b/fixtures/get-valid-system-boot-loaders-res.xml new file mode 100644 index 0000000..ffab829 --- /dev/null +++ b/fixtures/get-valid-system-boot-loaders-res.xml @@ -0,0 +1,11 @@ + + + + + + grub + pxelinux + + + + diff --git a/profile.go b/profile.go index e300a48..2fb2884 100644 --- a/profile.go +++ b/profile.go @@ -356,6 +356,12 @@ func (c *Client) GetProfileHandle(name string) (string, error) { return returnString(res, err) } +// GetValidProfileBootLoaders retrieves the list of bootloaders that can be assigned to a profile. +func (c *Client) GetValidProfileBootLoaders(profileName string) ([]string, error) { + resultUnmarshalled, err := c.Call("get_valid_profile_boot_loaders", profileName, c.Token) + return returnStringSlice(resultUnmarshalled, err) +} + // GetProfileAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. func (c *Client) GetProfileAsRendered(name string) (map[string]interface{}, error) { result, err := c.Call("get_profile_as_rendered", name, c.Token) diff --git a/profile_test.go b/profile_test.go index 4d2010a..e6da521 100644 --- a/profile_test.go +++ b/profile_test.go @@ -135,6 +135,16 @@ func TestGetProfileHandle(t *testing.T) { } } +func TestGetValidProfileBootLoaders(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-valid-profile-boot-loaders") + res, err := c.GetValidProfileBootLoaders("Ubuntu-20.04-x86_64") + FailOnError(t, err) + + if len(res) < 1 { + t.Error("Expected at least one boot loader.") + } +} + func TestGetProfileAsRendered(t *testing.T) { c := createStubHTTPClientSingle(t, "get-profile-as-rendered") res, err := c.GetProfileAsRendered("Ubuntu-20.04-x86_64") diff --git a/system.go b/system.go index 9ce81ab..d4b4fa0 100644 --- a/system.go +++ b/system.go @@ -592,6 +592,12 @@ func (c *Client) GetSystemHandle(name string) (string, error) { return returnString(res, err) } +// GetValidSystemBootLoaders retrieves the list of bootloaders that can be assigned to a system. +func (c *Client) GetValidSystemBootLoaders(systemName string) ([]string, error) { + resultUnmarshalled, err := c.Call("get_valid_system_boot_loaders", systemName, c.Token) + return returnStringSlice(resultUnmarshalled, err) +} + // GetSystemAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. func (c *Client) GetSystemAsRendered(name string) (map[string]interface{}, error) { result, err := c.Call("get_system_as_rendered", name, c.Token) diff --git a/system_test.go b/system_test.go index ef95ae0..e36cfaf 100644 --- a/system_test.go +++ b/system_test.go @@ -248,6 +248,16 @@ func TestModifyInterface(t *testing.T) { FailOnError(t, err) } +func TestGetValidSystemBootLoaders(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-valid-system-boot-loaders") + res, err := c.GetValidSystemBootLoaders("test") + FailOnError(t, err) + + if len(res) < 1 { + t.Error("Expected at least one boot loader.") + } +} + func TestGetSystemAsRendered(t *testing.T) { c := createStubHTTPClientSingle(t, "get-system-as-rendered") res, err := c.GetSystemAsRendered("test") From 9c921754282e4f69f4a96ff05d27206debcc3fd2 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:45:09 +0200 Subject: [PATCH 5/8] feat(repo): add GetRepoConfigForProfile and GetRepoConfigForSystem These wrap the get_repo_config_for_profile and get_repo_config_for_system XML-RPC endpoints which return the rendered yum/dnf repo file content for a given profile or system. Neither endpoint requires a token. Add request/response fixtures and unit tests. Co-Authored-By: Claude Sonnet 4.6 --- fixtures/get-repo-config-for-profile-req.xml | 11 +++++++++++ fixtures/get-repo-config-for-profile-res.xml | 13 +++++++++++++ fixtures/get-repo-config-for-system-req.xml | 11 +++++++++++ fixtures/get-repo-config-for-system-res.xml | 13 +++++++++++++ repo.go | 12 ++++++++++++ repo_test.go | 20 ++++++++++++++++++++ 6 files changed, 80 insertions(+) create mode 100644 fixtures/get-repo-config-for-profile-req.xml create mode 100644 fixtures/get-repo-config-for-profile-res.xml create mode 100644 fixtures/get-repo-config-for-system-req.xml create mode 100644 fixtures/get-repo-config-for-system-res.xml diff --git a/fixtures/get-repo-config-for-profile-req.xml b/fixtures/get-repo-config-for-profile-req.xml new file mode 100644 index 0000000..3f4e972 --- /dev/null +++ b/fixtures/get-repo-config-for-profile-req.xml @@ -0,0 +1,11 @@ + + + get_repo_config_for_profile + + + + testprof + + + + diff --git a/fixtures/get-repo-config-for-profile-res.xml b/fixtures/get-repo-config-for-profile-res.xml new file mode 100644 index 0000000..580fd99 --- /dev/null +++ b/fixtures/get-repo-config-for-profile-res.xml @@ -0,0 +1,13 @@ + + + + + [testrepo] +name=testrepo +baseurl=http://cobbler.example.com/cobbler/repo_mirror/testrepo +enabled=1 +gpgcheck=0 + + + + diff --git a/fixtures/get-repo-config-for-system-req.xml b/fixtures/get-repo-config-for-system-req.xml new file mode 100644 index 0000000..c0939a7 --- /dev/null +++ b/fixtures/get-repo-config-for-system-req.xml @@ -0,0 +1,11 @@ + + + get_repo_config_for_system + + + + testsys + + + + diff --git a/fixtures/get-repo-config-for-system-res.xml b/fixtures/get-repo-config-for-system-res.xml new file mode 100644 index 0000000..580fd99 --- /dev/null +++ b/fixtures/get-repo-config-for-system-res.xml @@ -0,0 +1,13 @@ + + + + + [testrepo] +name=testrepo +baseurl=http://cobbler.example.com/cobbler/repo_mirror/testrepo +enabled=1 +gpgcheck=0 + + + + diff --git a/repo.go b/repo.go index d7953e3..53c5398 100644 --- a/repo.go +++ b/repo.go @@ -271,6 +271,18 @@ func (c *Client) GetRepoHandle(name string) (string, error) { return returnString(res, err) } +// GetRepoConfigForProfile returns the rendered repository configuration for a profile. +func (c *Client) GetRepoConfigForProfile(profileName string) (string, error) { + result, err := c.Call("get_repo_config_for_profile", profileName) + return returnString(result, err) +} + +// GetRepoConfigForSystem returns the rendered repository configuration for a system. +func (c *Client) GetRepoConfigForSystem(systemName string) (string, error) { + result, err := c.Call("get_repo_config_for_system", systemName) + return returnString(result, err) +} + // GetRepoAsRendered returns the datastructure after it has passed through Cobblers inheritance structure. func (c *Client) GetRepoAsRendered(name string) (map[string]interface{}, error) { result, err := c.Call("get_repo_as_rendered", name, c.Token) diff --git a/repo_test.go b/repo_test.go index 83fb244..130bacc 100644 --- a/repo_test.go +++ b/repo_test.go @@ -165,6 +165,26 @@ func TestGetRepoHandle(t *testing.T) { } } +func TestGetRepoConfigForProfile(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-repo-config-for-profile") + res, err := c.GetRepoConfigForProfile("testprof") + FailOnError(t, err) + + if res == "" { + t.Error("Expected a non-empty repo config.") + } +} + +func TestGetRepoConfigForSystem(t *testing.T) { + c := createStubHTTPClientSingle(t, "get-repo-config-for-system") + res, err := c.GetRepoConfigForSystem("testsys") + FailOnError(t, err) + + if res == "" { + t.Error("Expected a non-empty repo config.") + } +} + func TestGetRepoAsRendered(t *testing.T) { c := createStubHTTPClientSingle(t, "get-repo-as-rendered") res, err := c.GetRepoAsRendered("rhel-7-x86_64") From e34900aa1152ecd78af322d0500825b998f3055f Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:51:53 +0200 Subject: [PATCH 6/8] feat(profile): add NewSubprofile Co-Authored-By: Claude Sonnet 4.6 --- fixtures/new-subprofile-req.xml | 11 +++++++++++ fixtures/new-subprofile-res.xml | 10 ++++++++++ profile.go | 7 +++++++ profile_test.go | 9 +++++++++ 4 files changed, 37 insertions(+) create mode 100644 fixtures/new-subprofile-req.xml create mode 100644 fixtures/new-subprofile-res.xml diff --git a/fixtures/new-subprofile-req.xml b/fixtures/new-subprofile-req.xml new file mode 100644 index 0000000..e0ff742 --- /dev/null +++ b/fixtures/new-subprofile-req.xml @@ -0,0 +1,11 @@ + + + new_subprofile + + + + securetoken99 + + + + diff --git a/fixtures/new-subprofile-res.xml b/fixtures/new-subprofile-res.xml new file mode 100644 index 0000000..025ab6f --- /dev/null +++ b/fixtures/new-subprofile-res.xml @@ -0,0 +1,10 @@ + + + + + + ___NEW___profile::subprof01== + + + + diff --git a/profile.go b/profile.go index 2fb2884..eb948d0 100644 --- a/profile.go +++ b/profile.go @@ -370,3 +370,10 @@ func (c *Client) GetProfileAsRendered(name string) (map[string]interface{}, erro } return result.(map[string]interface{}), nil } + +// NewSubprofile creates a new blank sub-profile on the server and returns its object ID. +// A sub-profile inherits from another profile (rather than a distro). +func (c *Client) NewSubprofile() (string, error) { + result, err := c.Call("new_subprofile", c.Token) + return returnString(result, err) +} diff --git a/profile_test.go b/profile_test.go index e6da521..3b7283e 100644 --- a/profile_test.go +++ b/profile_test.go @@ -154,3 +154,12 @@ func TestGetProfileAsRendered(t *testing.T) { t.Errorf("Wrong profile name returned: %v", res["name"]) } } + +func TestNewSubprofile(t *testing.T) { + c := createStubHTTPClientSingle(t, "new-subprofile") + res, err := c.NewSubprofile() + FailOnError(t, err) + if res == "" { + t.Error("Expected a non-empty object ID from NewSubprofile.") + } +} From 009a70949fd67444f08e44f85c25fa1a0f8f0f3e Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 23 Jun 2026 10:49:35 +0200 Subject: [PATCH 7/8] feat(system): add DisableNetboot, UploadLogData, ClearSystemLogs Co-Authored-By: Claude Sonnet 4.6 --- fixtures/clear-system-logs-req.xml | 16 +++++++++++++ fixtures/clear-system-logs-res.xml | 8 +++++++ fixtures/disable-netboot-req.xml | 16 +++++++++++++ fixtures/disable-netboot-res.xml | 8 +++++++ fixtures/upload-log-data-req.xml | 36 ++++++++++++++++++++++++++++++ fixtures/upload-log-data-res.xml | 8 +++++++ system.go | 18 +++++++++++++++ system_test.go | 24 ++++++++++++++++++++ 8 files changed, 134 insertions(+) create mode 100644 fixtures/clear-system-logs-req.xml create mode 100644 fixtures/clear-system-logs-res.xml create mode 100644 fixtures/disable-netboot-req.xml create mode 100644 fixtures/disable-netboot-res.xml create mode 100644 fixtures/upload-log-data-req.xml create mode 100644 fixtures/upload-log-data-res.xml diff --git a/fixtures/clear-system-logs-req.xml b/fixtures/clear-system-logs-req.xml new file mode 100644 index 0000000..90df395 --- /dev/null +++ b/fixtures/clear-system-logs-req.xml @@ -0,0 +1,16 @@ + + + clear_system_logs + + + + system::testsys + + + + + securetoken99 + + + + diff --git a/fixtures/clear-system-logs-res.xml b/fixtures/clear-system-logs-res.xml new file mode 100644 index 0000000..d02b2d3 --- /dev/null +++ b/fixtures/clear-system-logs-res.xml @@ -0,0 +1,8 @@ + + + + + 1 + + + diff --git a/fixtures/disable-netboot-req.xml b/fixtures/disable-netboot-req.xml new file mode 100644 index 0000000..a5a5b40 --- /dev/null +++ b/fixtures/disable-netboot-req.xml @@ -0,0 +1,16 @@ + + + disable_netboot + + + + testsys + + + + + securetoken99 + + + + diff --git a/fixtures/disable-netboot-res.xml b/fixtures/disable-netboot-res.xml new file mode 100644 index 0000000..d02b2d3 --- /dev/null +++ b/fixtures/disable-netboot-res.xml @@ -0,0 +1,8 @@ + + + + + 1 + + + diff --git a/fixtures/upload-log-data-req.xml b/fixtures/upload-log-data-req.xml new file mode 100644 index 0000000..c02f025 --- /dev/null +++ b/fixtures/upload-log-data-req.xml @@ -0,0 +1,36 @@ + + + upload_log_data + + + + testsys + + + + + /var/log/cobbler/testsys.log + + + + + 12 + + + + + 0 + + + + + hello world! + + + + + securetoken99 + + + + diff --git a/fixtures/upload-log-data-res.xml b/fixtures/upload-log-data-res.xml new file mode 100644 index 0000000..d02b2d3 --- /dev/null +++ b/fixtures/upload-log-data-res.xml @@ -0,0 +1,8 @@ + + + + + 1 + + + diff --git a/system.go b/system.go index d4b4fa0..936cb3d 100644 --- a/system.go +++ b/system.go @@ -592,6 +592,24 @@ func (c *Client) GetSystemHandle(name string) (string, error) { return returnString(res, err) } +// DisableNetboot disables PXE booting for the named system (pxe_just_once feature). +func (c *Client) DisableNetboot(name string) error { + _, err := c.Call("disable_netboot", name, c.Token) + return err +} + +// UploadLogData uploads Anaconda log data for the named system (anamon logging). +func (c *Client) UploadLogData(sysName, file string, size, offset int, data string) (bool, error) { + result, err := c.Call("upload_log_data", sysName, file, size, offset, data, c.Token) + return returnBool(result, err) +} + +// ClearSystemLogs clears the console logs for the system identified by its object ID. +func (c *Client) ClearSystemLogs(objectId string) (bool, error) { + result, err := c.Call("clear_system_logs", objectId, c.Token) + return returnBool(result, err) +} + // GetValidSystemBootLoaders retrieves the list of bootloaders that can be assigned to a system. func (c *Client) GetValidSystemBootLoaders(systemName string) ([]string, error) { resultUnmarshalled, err := c.Call("get_valid_system_boot_loaders", systemName, c.Token) diff --git a/system_test.go b/system_test.go index e36cfaf..e618658 100644 --- a/system_test.go +++ b/system_test.go @@ -349,3 +349,27 @@ func TestRenameInterface(t *testing.T) { // Assert FailOnError(t, err) } + +func TestDisableNetboot(t *testing.T) { + c := createStubHTTPClientSingle(t, "disable-netboot") + err := c.DisableNetboot("testsys") + FailOnError(t, err) +} + +func TestUploadLogData(t *testing.T) { + c := createStubHTTPClientSingle(t, "upload-log-data") + res, err := c.UploadLogData("testsys", "/var/log/cobbler/testsys.log", 12, 0, "hello world!") + FailOnError(t, err) + if !res { + t.Error("Expected true result from UploadLogData.") + } +} + +func TestClearSystemLogs(t *testing.T) { + c := createStubHTTPClientSingle(t, "clear-system-logs") + res, err := c.ClearSystemLogs("system::testsys") + FailOnError(t, err) + if !res { + t.Error("Expected true result from ClearSystemLogs.") + } +} From 25e7d4094f51e3fbe7f312d67af82e2d0bd136a8 Mon Sep 17 00:00:00 2001 From: Enno Gotthold Date: Tue, 9 Jun 2026 11:21:30 +0200 Subject: [PATCH 8/8] Fix orphan-return methods to return their actual result --- cobblerclient.go | 118 +++++++++++++++++++++++--------- cobblerclient_test.go | 14 ++-- fixtures/get-random-mac-req.xml | 4 ++ item.go | 17 +++-- 4 files changed, 110 insertions(+), 43 deletions(-) diff --git a/cobblerclient.go b/cobblerclient.go index b0fdf8e..2fd5802 100644 --- a/cobblerclient.go +++ b/cobblerclient.go @@ -20,13 +20,14 @@ import ( "bytes" "errors" "fmt" - "github.com/go-viper/mapstructure/v2" - "github.com/kolo/xmlrpc" "io" "net/http" "reflect" "sort" "strings" + + "github.com/go-viper/mapstructure/v2" + "github.com/kolo/xmlrpc" ) const bodyTypeXML = "text/xml" @@ -157,11 +158,15 @@ func (c *Client) Ping() (bool, error) { } } -// AutoAddRepos automatically imports any repos server side that are known to the daemon. It is the responsitbility -// of the caller to execute [Client.BackgroundReposync]. -func (c *Client) AutoAddRepos() error { - _, err := c.Call("auto_add_repos", c.Token) - return err +// AutoAddRepos automatically imports any repos server-side that are known to the daemon. +// Returns whether the operation succeeded. The caller is responsible for following up +// with [Client.BackgroundReposync]. +func (c *Client) AutoAddRepos() (bool, error) { + result, err := c.Call("auto_add_repos", c.Token) + if err != nil { + return false, err + } + return result.(bool), nil } // GetAutoinstallTemplates retrieves a list of all templates that are in use by Cobbler. @@ -176,10 +181,13 @@ func (c *Client) GetAutoinstallSnippets() error { return err } -// IsAutoinstallInUse checks if a given system has reported that it is currently installing. -func (c *Client) IsAutoinstallInUse(name string) error { - _, err := c.Call("is_autoinstall_in_use", name, c.Token) - return err +// IsAutoinstallInUse reports whether the named system is currently installing. +func (c *Client) IsAutoinstallInUse(name string) (bool, error) { + result, err := c.Call("is_autoinstall_in_use", name, c.Token) + if err != nil { + return false, err + } + return result.(bool), nil } // GenerateIPxe generates the iPXE (formerly gPXE) configuration data. @@ -206,36 +214,82 @@ func (c *Client) GetBlendedData(profile, system string) (map[string]interface{}, return result.(map[string]interface{}), err } -// RegisterNewSystem registers a new system without a Cobbler token. This is normally called -// during unattended installation by a script. -func (c *Client) RegisterNewSystem(info map[string]interface{}) error { - _, err := c.Call("register_new_system", info, c.Token) - return err +// RegisterNewSystem registers a new system without a Cobbler token. Normally +// called during unattended installation by a script. Returns the backend's +// status code (0 on success). +func (c *Client) RegisterNewSystem(info map[string]interface{}) (int, error) { + result, err := c.Call("register_new_system", info, c.Token) + if err != nil { + return -1, err + } + return convertToInt(result) } -// RunInstallTriggers runs installation triggers for a given object. This is normally called during -// unattended installation. -func (c *Client) RunInstallTriggers(mode string, objtype string, name string, ip string) error { - _, err := c.Call("run_install_triggers", mode, objtype, name, ip, c.Token) - return err +// RunInstallTriggers runs installation triggers for a given object. Normally +// called during unattended installation. mode is "pre", "post", or "firstboot"; +// objtype is "system" or "profile". Returns whether the trigger ran successfully. +func (c *Client) RunInstallTriggers(mode, objtype, name, ip string) (bool, error) { + result, err := c.Call("run_install_triggers", mode, objtype, name, ip, c.Token) + if err != nil { + return false, err + } + return result.(bool), nil } -// GetReposCompatibleWithProfile returns all repositories that can be potentially assigned to a given profile. -func (c *Client) GetReposCompatibleWithProfile(profile_name string) error { - _, err := c.Call("get_repos_compatible_with_profile", profile_name, c.Token) - return err +// GetReposCompatibleWithProfile returns all repositories that can be assigned +// to the named profile (filtered by arch compatibility with the profile's distro). +func (c *Client) GetReposCompatibleWithProfile(profileName string) ([]map[string]interface{}, error) { + result, err := c.Call("get_repos_compatible_with_profile", profileName, c.Token) + if err != nil { + return nil, err + } + raw, ok := result.([]interface{}) + if !ok { + return nil, fmt.Errorf("get_repos_compatible_with_profile returned %T, want list", result) + } + out := make([]map[string]interface{}, 0, len(raw)) + for _, v := range raw { + if m, ok := v.(map[string]interface{}); ok { + out = append(out, m) + } + } + return out, nil } -// FindSystemByDnsName searches for a system with a given DNS name. -func (c *Client) FindSystemByDnsName(dns_name string) error { - _, err := c.Call("find_system_by_dns_name", dns_name) - return err +// FindSystemByDnsName searches for a system by DNS name. Returns the rendered +// system dict or an empty map if not found. +func (c *Client) FindSystemByDnsName(dnsName string) (map[string]interface{}, error) { + result, err := c.Call("find_system_by_dns_name", dnsName) + if err != nil { + return nil, err + } + if m, ok := result.(map[string]interface{}); ok { + return m, nil + } + return map[string]interface{}{}, nil +} + +// GetRandomMacFor returns a random MAC address tailored for the given virt_type. +// Valid values per Python signature: "qemu", "kvm", "xenpv", "xenfv", "vmware", +// "vmwarew", "openvz", "auto". +func (c *Client) GetRandomMacFor(virtType string) (string, error) { + if virtType == "" { + virtType = "kvm" + } + result, err := c.Call("get_random_mac", virtType, c.Token) + if err != nil { + return "", err + } + s, ok := result.(string) + if !ok { + return "", fmt.Errorf("get_random_mac returned %T, want string", result) + } + return s, nil } // GetRandomMac generates a random MAC address for use with a virtualized system. -func (c *Client) GetRandomMac() error { - _, err := c.Call("get_random_mac") - return err +func (c *Client) GetRandomMac() (string, error) { + return c.GetRandomMacFor("xenpv") } type StatusOption string diff --git a/cobblerclient_test.go b/cobblerclient_test.go index c628569..e0f78c9 100644 --- a/cobblerclient_test.go +++ b/cobblerclient_test.go @@ -83,7 +83,7 @@ func TestPing(t *testing.T) { func TestAutoAddRepos(t *testing.T) { c := createStubHTTPClientSingle(t, "auto-add-repos") - err := c.AutoAddRepos() + _, err := c.AutoAddRepos() FailOnError(t, err) } @@ -110,7 +110,7 @@ func TestGetAutoinstallSnippets(t *testing.T) { func TestIsAutoinstallInUse(t *testing.T) { c := createStubHTTPClientSingle(t, "is-autoinstall-in-use") - err := c.IsAutoinstallInUse("") + _, err := c.IsAutoinstallInUse("") FailOnError(t, err) } @@ -151,7 +151,7 @@ func TestRegisterNewSystem(t *testing.T) { c := createStubHTTPClientSingle(t, "register-new-system") - err := c.RegisterNewSystem( + _, err := c.RegisterNewSystem( map[string]interface{}{ "name": "test", "profile": "testprof", @@ -168,7 +168,7 @@ func TestRegisterNewSystem(t *testing.T) { func TestRunInstallTriggers(t *testing.T) { c := createStubHTTPClientSingle(t, "run-install-triggers") - err := c.RunInstallTriggers("", "", "", "") + _, err := c.RunInstallTriggers("", "", "", "") FailOnError(t, err) } @@ -178,7 +178,7 @@ func TestGetReposCompatibleWithProfile(t *testing.T) { "get-repos-compatible-with-profile", ) - err := c.GetReposCompatibleWithProfile("testprof") + _, err := c.GetReposCompatibleWithProfile("testprof") FailOnError(t, err) } @@ -188,14 +188,14 @@ func TestFindSystemByDnsName(t *testing.T) { "find-system-by-dns-name", ) - err := c.FindSystemByDnsName("testname") + _, err := c.FindSystemByDnsName("testname") FailOnError(t, err) } func TestGetRandomMac(t *testing.T) { c := createStubHTTPClientSingle(t, "get-random-mac") - err := c.GetRandomMac() + _, err := c.GetRandomMac() FailOnError(t, err) } diff --git a/fixtures/get-random-mac-req.xml b/fixtures/get-random-mac-req.xml index 2933a15..f1abe30 100644 --- a/fixtures/get-random-mac-req.xml +++ b/fixtures/get-random-mac-req.xml @@ -1,4 +1,8 @@ get_random_mac + + xenpv + securetoken99 + diff --git a/item.go b/item.go index 04de825..17922d6 100644 --- a/item.go +++ b/item.go @@ -149,10 +149,19 @@ func (c *Client) GetItemNames(what string) ([]string, error) { return returnStringSlice(resultUnmarshalled, err) } -// GetItemResolvedValue retrieves the value of a single attribute of a single item which was passed through the -// inheritance chain of Cobbler. -func (c *Client) GetItemResolvedValue(itemUuid string, attribute string) error { - _, err := c.Call("get_item_resolved_value", itemUuid, attribute) +// GetItemResolvedValue returns the value of a single attribute of a single +// item with the inheritance chain applied. attribute is the dotted path to +// the property (e.g. []string{"ipv4", "address"} for a NetworkInterface). +// Cobbler 4.0.0 returns Union[str, int, float, List, dict]. +func (c *Client) GetItemResolvedValue(itemUuid string, attribute []string) (interface{}, error) { + return c.Call("get_item_resolved_value", itemUuid, attribute) +} + +// SetItemResolvedValue sets a single attribute on an item with inheritance +// rules applied. attribute is the dotted path; value is the desired value. +// Added in Cobbler 4.0.0. +func (c *Client) SetItemResolvedValue(itemUuid string, attribute []string, value interface{}) error { + _, err := c.Call("set_item_resolved_value", itemUuid, attribute, value, c.Token) return err }