-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[FEATURE] Add support for managing computer groups (#20)
* Support for static computer groups --------- Co-authored-by: William Clot <[email protected]> Co-authored-by: William Clot <[email protected]>
- Loading branch information
1 parent
865e929
commit 5ac589b
Showing
8 changed files
with
352 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
package classic | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/xml" | ||
"fmt" | ||
"net/http" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
// ComputerGroups represents a list of computer groups in Jamf | ||
func (j *Client) ComputerGroups() ([]BasicComputerGroupInfo, error) { | ||
ep := fmt.Sprintf("%s/%s", j.Endpoint, computerGroupsContext) | ||
req, err := http.NewRequestWithContext(context.Background(), "GET", ep, nil) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error building Jamf computer groups query request") | ||
} | ||
res := ComputerGroups{} | ||
if err := j.makeAPIrequest(req, &res); err != nil { | ||
return nil, errors.Wrapf(err, "unable to query available computer groups from %s", ep) | ||
} | ||
return res.List, nil | ||
} | ||
|
||
// ComputerGroupDetails returns the details for a specific group given its ID or Name | ||
func (j *Client) ComputerGroupDetails(identifier any) (*ComputerGroup, error) { | ||
ep, err := EndpointBuilder(j.Endpoint, computerGroupsContext, identifier) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF query request endpoint for computer group: %v", identifier) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(context.Background(), "GET", ep, nil) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF query request for computer group: %v", identifier) | ||
} | ||
|
||
res := ComputerGroup{} | ||
if err := j.makeAPIrequest(req, &res); err != nil { | ||
return nil, errors.Wrapf(err, "unable to query computer group with ID: %d from %s", identifier, ep) | ||
} | ||
return &res, nil | ||
} | ||
|
||
// UpdateComputerGroupMembers will update the members of a computer group in Jamf by either group ID or group Name | ||
func (j *Client) UpdateComputerGroupMembers(identifier any, updates *ComputerGroupBindingChanges) (*ComputerGroupDetails, error) { | ||
ep, err := EndpointBuilder(j.Endpoint, computerGroupsContext, identifier) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF query request for computer group: %v", identifier) | ||
} | ||
|
||
bodyContent, err := xml.Marshal(updates) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF update payload for computer group: %v", identifier) | ||
} | ||
|
||
body := bytes.NewReader(bodyContent) | ||
req, err := http.NewRequestWithContext(context.Background(), "PUT", ep, body) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF update request for computer group: %v (%s)", identifier, ep) | ||
} | ||
|
||
res := ComputerGroupDetails{} | ||
if err := j.makeAPIrequest(req, &res); err != nil { | ||
return nil, errors.Wrapf(err, "unable to process JAMF update request for computer group: %v (%s)", identifier, ep) | ||
} | ||
|
||
return &res, nil | ||
} | ||
|
||
func (j *Client) CreateComputerGroup(newGroup *ComputerGroupDetails) (*ComputerGroupDetails, error) { | ||
ep, err := EndpointBuilder(j.Endpoint, computerGroupsContext, -1) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error building JAMF add computer group request endpoint") | ||
} | ||
|
||
if newGroup.Name == "" { | ||
return nil, errors.New("error building JAMF add computer group request: group name is required") | ||
} | ||
|
||
bodyContent, err := xml.Marshal(newGroup) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "error building JAMF add computer group payload") | ||
} | ||
|
||
body := bytes.NewReader(bodyContent) | ||
req, err := http.NewRequestWithContext(context.Background(), "POST", ep, body) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF add computer group request") | ||
} | ||
|
||
res := ComputerGroupDetails{} | ||
if err := j.makeAPIrequest(req, &res); err != nil { | ||
return nil, errors.Wrap(err, "unable to process JAMF add computer group request") | ||
} | ||
|
||
return &res, nil | ||
} | ||
|
||
func (j *Client) DeleteComputerGroup(identifier any) (*ComputerGroupDetails, error) { | ||
ep, err := EndpointBuilder(j.Endpoint, computerGroupsContext, identifier) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF delete computer group request endpoint for group: %v", identifier) | ||
} | ||
|
||
req, err := http.NewRequestWithContext(context.Background(), "DELETE", ep, nil) | ||
if err != nil { | ||
return nil, errors.Wrapf(err, "error building JAMF delete computer group request for group: %v", identifier) | ||
} | ||
|
||
res := ComputerGroupDetails{} | ||
if err := j.makeAPIrequest(req, &res); err != nil { | ||
return nil, errors.Wrapf(err, "unable to process JAMF delete computer group request for group: %v", identifier) | ||
} | ||
|
||
return &res, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package classic | ||
|
||
import "encoding/xml" | ||
|
||
type ComputerGroups struct { | ||
List []BasicComputerGroupInfo `json:"computer_groups" xml:"computer_groups>computer_group,omitempty"` | ||
Size int `json:"size" xml:"size"` | ||
} | ||
|
||
// ComputerGroup represents a group a device is a member of in Jamf | ||
type ComputerGroup struct { | ||
Info ComputerGroupDetails `json:"computer_group" xml:"computer_group,omitempty"` | ||
} | ||
|
||
// BasicComputerGroupInfo represents the information returned in a list of all | ||
// computer groups from Jamf | ||
type BasicComputerGroupInfo struct { | ||
ID int `json:"id,omitempty" xml:"id,omitempty"` | ||
Name string `json:"name,omitempty" xml:"name"` | ||
IsSmart bool `json:"is_smart" xml:"is_smart"` | ||
} | ||
|
||
// ComputerGroupDetails represents the detailed information for a specific computer group | ||
type ComputerGroupDetails struct { | ||
XMLName xml.Name `json:"computer_group" xml:"computer_group,omitempty"` | ||
BasicComputerGroupInfo | ||
Computers []BasicComputerInfo `json:"computers" xml:"computers>computer,omitempty"` | ||
} | ||
|
||
// ComputerGroupBindingChanges represents the changes to a computer group binding when | ||
// updating the members of a computer group in Jamf | ||
type ComputerGroupBindingChanges struct { | ||
XMLName xml.Name `json:"-" xml:"computer_group,omitempty"` | ||
Additions []GeneralInformation `xml:"computer_additions>computer"` | ||
Removals []GeneralInformation `xml:"computer_deletions>computer"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
package classic_test | ||
|
||
import ( | ||
"encoding/xml" | ||
"fmt" | ||
"io" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
jamf "github.com/DataDog/jamf-api-client-go/classic" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
var COMPUTER_GROUPS_BASE_API_ENDPOINT = "/JSSResource/computergroups" | ||
|
||
func computerGroupsResponseMocks(t *testing.T) *httptest.Server { | ||
var resp string | ||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
switch r.RequestURI { | ||
case COMPUTER_GROUPS_BASE_API_ENDPOINT: | ||
fmt.Fprintf(w, `{ | ||
"computer_groups": [ | ||
{ | ||
"id": 1, | ||
"name": "Test Group 1", | ||
"is_smart": false | ||
}, | ||
{ | ||
"id": 2, | ||
"name": "Test Group 2", | ||
"is_smart": true | ||
}, | ||
{ | ||
"id": 3, | ||
"name": "Test Group 3", | ||
"is_smart": false | ||
}] | ||
}`) | ||
case fmt.Sprintf("%s/id/1", COMPUTER_GROUPS_BASE_API_ENDPOINT), fmt.Sprintf("%s/id/-1", COMPUTER_GROUPS_BASE_API_ENDPOINT), fmt.Sprintf("%s/name/Test%sGroup%s1", COMPUTER_GROUPS_BASE_API_ENDPOINT, "%20", "%20"): | ||
switch r.Method { | ||
case "GET": | ||
w.Header().Add("Content-Type", "application/xml") | ||
mockGroup := &jamf.ComputerGroup{ | ||
Info: jamf.ComputerGroupDetails{ | ||
BasicComputerGroupInfo: jamf.BasicComputerGroupInfo{ | ||
ID: 1, | ||
Name: "Test Group 1", | ||
IsSmart: false, | ||
}, | ||
Computers: []jamf.BasicComputerInfo{ | ||
{ | ||
GeneralInformation: jamf.GeneralInformation{ | ||
ID: 1, | ||
Name: "Test Computer 1", | ||
}, | ||
}, | ||
}, | ||
}, | ||
} | ||
groupData, err := xml.MarshalIndent(mockGroup, "", " ") | ||
if err != nil { | ||
fmt.Fprintf(w, err.Error()) | ||
} | ||
fmt.Fprintf(w, string(groupData)) | ||
case "POST": | ||
w.Header().Add("Content-Type", "application/xml") | ||
data, err := io.ReadAll(r.Body) | ||
if err != nil { | ||
fmt.Fprintf(w, err.Error()) | ||
} | ||
groupContents := &jamf.ComputerGroupDetails{} | ||
err = xml.Unmarshal(data, groupContents) | ||
if err != nil { | ||
fmt.Fprintf(w, err.Error()) | ||
} | ||
groupData, err := xml.MarshalIndent(groupContents, "", " ") | ||
if err != nil { | ||
fmt.Fprintf(w, err.Error()) | ||
} | ||
fmt.Fprintf(w, string(groupData)) | ||
default: | ||
w.Header().Add("Content-Type", "application/xml") | ||
mockGroup := &jamf.ComputerGroupDetails{ | ||
BasicComputerGroupInfo: jamf.BasicComputerGroupInfo{ | ||
ID: 1, | ||
Name: "Test Group 1", | ||
IsSmart: false, | ||
}, | ||
Computers: []jamf.BasicComputerInfo{ | ||
{ | ||
GeneralInformation: jamf.GeneralInformation{ | ||
ID: 1, | ||
Name: "Test Computer 1", | ||
}, | ||
}, | ||
}, | ||
} | ||
groupData, err := xml.MarshalIndent(mockGroup, "", " ") | ||
if err != nil { | ||
fmt.Fprintf(w, err.Error()) | ||
} | ||
fmt.Fprintf(w, string(groupData)) | ||
} | ||
default: | ||
http.Error(w, fmt.Sprintf("bad Jamf API %s call to %s", r.Method, r.URL), http.StatusInternalServerError) | ||
return | ||
} | ||
_, err := w.Write([]byte(resp)) | ||
assert.Nil(t, err) | ||
})) | ||
} | ||
|
||
func TestListAllComputerGroups(t *testing.T) { | ||
server := computerGroupsResponseMocks(t) | ||
defer server.Close() | ||
j, err := jamf.NewClient(server.URL, "test", "test", server.Client(), jamf.WithTokenAuth()) | ||
assert.Nil(t, err) | ||
grps, err := j.ComputerGroups() | ||
assert.Nil(t, err) | ||
assert.Equal(t, 3, len(grps)) | ||
assert.Equal(t, 1, grps[0].ID) | ||
assert.Equal(t, "Test Group 1", grps[0].Name) | ||
assert.Equal(t, false, grps[0].IsSmart) | ||
assert.Equal(t, 2, grps[1].ID) | ||
assert.Equal(t, "Test Group 2", grps[1].Name) | ||
assert.Equal(t, true, grps[1].IsSmart) | ||
assert.Equal(t, 3, grps[2].ID) | ||
assert.Equal(t, "Test Group 3", grps[2].Name) | ||
assert.Equal(t, false, grps[2].IsSmart) | ||
} | ||
|
||
func TestQuerySpecificComputerGroups(t *testing.T) { | ||
server := computerGroupsResponseMocks(t) | ||
defer server.Close() | ||
j, err := jamf.NewClient(server.URL, "test", "test", server.Client(), jamf.WithTokenAuth()) | ||
assert.Nil(t, err) | ||
grp, err := j.ComputerGroupDetails(1) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 1, grp.Info.ID) | ||
assert.Equal(t, "Test Group 1", grp.Info.Name) | ||
assert.Equal(t, false, grp.Info.IsSmart) | ||
assert.Equal(t, 1, grp.Info.Computers[0].ID) | ||
assert.Equal(t, "Test Computer 1", grp.Info.Computers[0].Name) | ||
} | ||
|
||
func TestCreateComputerGroup(t *testing.T) { | ||
server := computerGroupsResponseMocks(t) | ||
defer server.Close() | ||
j, err := jamf.NewClient(server.URL, "test", "test", server.Client(), jamf.WithTokenAuth()) | ||
assert.Nil(t, err) | ||
grp := &jamf.ComputerGroupDetails{ | ||
BasicComputerGroupInfo: jamf.BasicComputerGroupInfo{ | ||
Name: "Unit Test Group", | ||
IsSmart: false, | ||
}, | ||
Computers: []jamf.BasicComputerInfo{}, | ||
} | ||
createdGrp, err := j.CreateComputerGroup(grp) | ||
assert.Nil(t, err) | ||
assert.Equal(t, "Unit Test Group", createdGrp.Name) | ||
assert.Equal(t, false, createdGrp.IsSmart) | ||
assert.Equal(t, 0, len(createdGrp.Computers)) | ||
} | ||
|
||
func TestDeleteComputerGroup(t *testing.T) { | ||
server := computerGroupsResponseMocks(t) | ||
defer server.Close() | ||
j, err := jamf.NewClient(server.URL, "test", "test", server.Client(), jamf.WithTokenAuth()) | ||
assert.Nil(t, err) | ||
deletedGrp, err := j.DeleteComputerGroup(1) | ||
assert.Nil(t, err) | ||
assert.Equal(t, 1, deletedGrp.ID) | ||
assert.Equal(t, "Test Group 1", deletedGrp.Name) | ||
assert.Equal(t, false, deletedGrp.IsSmart) | ||
|
||
deletedGrp, err = j.DeleteComputerGroup("Test%20Group%201") | ||
assert.Nil(t, err) | ||
assert.Equal(t, 1, deletedGrp.ID) | ||
assert.Equal(t, "Test Group 1", deletedGrp.Name) | ||
assert.Equal(t, false, deletedGrp.IsSmart) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.