mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Extract Android disk storage data (#32133)
Implements Android storage data extraction for issue #27080.
This commit is contained in:
parent
de6ef0544b
commit
8212180819
7 changed files with 305 additions and 16 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -108,3 +108,7 @@ tools/test_extensions/hello_world/windows-arm64
|
|||
|
||||
# Residual files when building fleet_tables extension.
|
||||
fleet_tables_*.ext
|
||||
|
||||
# Local dev files
|
||||
.env
|
||||
.tool-versions
|
||||
|
|
|
|||
|
|
@ -168,9 +168,17 @@ const HostSummary = ({
|
|||
);
|
||||
|
||||
const renderDiskSpaceSummary = () => {
|
||||
const title = isAndroidHost ? (
|
||||
<TooltipWrapper tipContent="Includes internal and removable storage (e.g. microSD card).">
|
||||
Disk space
|
||||
</TooltipWrapper>
|
||||
) : (
|
||||
"Disk space"
|
||||
);
|
||||
|
||||
return (
|
||||
<DataSet
|
||||
title="Disk space"
|
||||
title={title}
|
||||
value={
|
||||
<DiskSpaceIndicator
|
||||
baseClass="info-flex"
|
||||
|
|
|
|||
|
|
@ -120,6 +120,18 @@ func (ds *Datastore) NewAndroidHost(ctx context.Context, host *fleet.AndroidHost
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "creating new Android device")
|
||||
}
|
||||
|
||||
// insert storage data into host_disks table for API consumption
|
||||
if host.Host.GigsTotalDiskSpace > 0 || host.Host.GigsDiskSpaceAvailable > 0 {
|
||||
err = ds.SetOrUpdateHostDisksSpace(ctx, host.Host.ID,
|
||||
host.Host.GigsDiskSpaceAvailable,
|
||||
host.Host.PercentDiskSpaceAvailable,
|
||||
host.Host.GigsTotalDiskSpace)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "setting Android host disk space")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return host, err
|
||||
|
|
@ -195,6 +207,18 @@ func (ds *Datastore) UpdateAndroidHost(ctx context.Context, host *fleet.AndroidH
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "update Android device")
|
||||
}
|
||||
|
||||
// update storage data in host_disks table for API consumption
|
||||
if host.Host.GigsTotalDiskSpace > 0 || host.Host.GigsDiskSpaceAvailable > 0 {
|
||||
err = ds.SetOrUpdateHostDisksSpace(ctx, host.Host.ID,
|
||||
host.Host.GigsDiskSpaceAvailable,
|
||||
host.Host.PercentDiskSpaceAvailable,
|
||||
host.Host.GigsTotalDiskSpace)
|
||||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "updating Android host disk space")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ func TestAndroid(t *testing.T) {
|
|||
{"NewAndroidHost", testNewAndroidHost},
|
||||
{"UpdateAndroidHost", testUpdateAndroidHost},
|
||||
{"AndroidMDMStats", testAndroidMDMStats},
|
||||
{"AndroidHostStorageData", testAndroidHostStorageData},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
|
|
@ -287,3 +288,72 @@ func testAndroidMDMStats(t *testing.T, ds *Datastore) {
|
|||
require.Equal(t, 1, solutionsStats[0].HostsCount)
|
||||
require.Equal(t, serverURL, solutionsStats[0].ServerURL)
|
||||
}
|
||||
|
||||
func testAndroidHostStorageData(t *testing.T, ds *Datastore) {
|
||||
test.AddBuiltinLabels(t, ds)
|
||||
|
||||
// Android host with storage data
|
||||
const enterpriseSpecificID = "storage_test_enterprise"
|
||||
host := &fleet.AndroidHost{
|
||||
Host: &fleet.Host{
|
||||
Hostname: "android-storage-test",
|
||||
ComputerName: "Android Storage Test Device",
|
||||
Platform: "android",
|
||||
OSVersion: "Android 14",
|
||||
Build: "UPB4.230623.005",
|
||||
Memory: 8192, // 8GB RAM
|
||||
TeamID: nil,
|
||||
HardwareSerial: "STORAGE-TEST-SERIAL",
|
||||
CPUType: "arm64-v8a",
|
||||
HardwareModel: "Google Pixel 8 Pro",
|
||||
HardwareVendor: "Google",
|
||||
GigsTotalDiskSpace: 128.0, // 64GB system + 64GB external
|
||||
GigsDiskSpaceAvailable: 35.0, // 10GB + 25GB available
|
||||
PercentDiskSpaceAvailable: 27.34, // 35/128 * 100
|
||||
},
|
||||
Device: &android.Device{
|
||||
DeviceID: "storage-test-device-id",
|
||||
EnterpriseSpecificID: ptr.String(enterpriseSpecificID),
|
||||
AndroidPolicyID: ptr.Uint(1),
|
||||
LastPolicySyncTime: ptr.Time(time.Now().UTC().Truncate(time.Millisecond)),
|
||||
},
|
||||
}
|
||||
host.SetNodeKey(enterpriseSpecificID)
|
||||
|
||||
// NewAndroidHost with storage data
|
||||
result, err := ds.NewAndroidHost(testCtx(), host)
|
||||
require.NoError(t, err)
|
||||
require.NotZero(t, result.Host.ID)
|
||||
|
||||
// storage data was saved correctly
|
||||
assert.Equal(t, 128.0, result.Host.GigsTotalDiskSpace, "Total disk space should be saved")
|
||||
assert.Equal(t, 35.0, result.Host.GigsDiskSpaceAvailable, "Available disk space should be saved")
|
||||
assert.Equal(t, 27.34, result.Host.PercentDiskSpaceAvailable, "Disk space percentage should be saved")
|
||||
|
||||
// AndroidHostLite provides lightweight Android data (no storage data)
|
||||
resultLite, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, result.Host.ID, resultLite.Host.ID)
|
||||
|
||||
// UpdateAndroidHost preserves storage data
|
||||
updatedHost := result
|
||||
updatedHost.Host.Hostname = "updated-hostname"
|
||||
updatedHost.Host.GigsTotalDiskSpace = 256.0 // Updated: 128GB system + 128GB external
|
||||
updatedHost.Host.GigsDiskSpaceAvailable = 64.0 // Updated: 20GB + 44GB available
|
||||
updatedHost.Host.PercentDiskSpaceAvailable = 25.0 // Updated: 64/256 * 100
|
||||
|
||||
err = ds.UpdateAndroidHost(testCtx(), updatedHost, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// verify updated host data via host query (includes storage from host_disks)
|
||||
finalResult, err := ds.AndroidHostLite(testCtx(), enterpriseSpecificID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// get host data to check storage updates
|
||||
updatedFullHost, err := ds.Host(testCtx(), finalResult.Host.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "updated-hostname", updatedFullHost.Hostname, "Hostname should be updated")
|
||||
assert.Equal(t, 256.0, updatedFullHost.GigsTotalDiskSpace, "Updated total disk space should be saved in host_disks")
|
||||
assert.Equal(t, 64.0, updatedFullHost.GigsDiskSpaceAvailable, "Updated available disk space should be saved in host_disks")
|
||||
assert.Equal(t, 25.0, updatedFullHost.PercentDiskSpaceAvailable, "Updated disk space percentage should be saved in host_disks")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -254,6 +254,27 @@ func (svc *Service) updateHost(ctx context.Context, device *androidmanagement.De
|
|||
host.Host.OSVersion = "Android " + device.SoftwareInfo.AndroidVersion
|
||||
host.Host.Build = device.SoftwareInfo.AndroidBuildNumber
|
||||
host.Host.Memory = device.MemoryInfo.TotalRam
|
||||
|
||||
if device.MemoryInfo.TotalInternalStorage > 0 {
|
||||
totalStorageBytes := device.MemoryInfo.TotalInternalStorage
|
||||
|
||||
var totalAvailableBytes int64
|
||||
for _, event := range device.MemoryEvents {
|
||||
switch event.EventType {
|
||||
case "EXTERNAL_STORAGE_DETECTED":
|
||||
totalStorageBytes += event.ByteCount
|
||||
case "INTERNAL_STORAGE_MEASURED", "EXTERNAL_STORAGE_MEASURED":
|
||||
totalAvailableBytes += event.ByteCount
|
||||
}
|
||||
}
|
||||
|
||||
if totalStorageBytes > 0 {
|
||||
host.Host.GigsTotalDiskSpace = float64(totalStorageBytes) / (1024 * 1024 * 1024)
|
||||
host.Host.GigsDiskSpaceAvailable = float64(totalAvailableBytes) / (1024 * 1024 * 1024)
|
||||
host.Host.PercentDiskSpaceAvailable = (float64(totalAvailableBytes) / float64(totalStorageBytes)) * 100
|
||||
}
|
||||
}
|
||||
|
||||
host.Host.HardwareSerial = device.HardwareInfo.SerialNumber
|
||||
host.Host.CPUType = device.HardwareInfo.Hardware
|
||||
host.Host.HardwareModel = svc.getComputerName(device)
|
||||
|
|
@ -291,22 +312,47 @@ func (svc *Service) addNewHost(ctx context.Context, device *androidmanagement.De
|
|||
if err != nil {
|
||||
return ctxerr.Wrap(ctx, err, "getting device ID")
|
||||
}
|
||||
|
||||
var gigsTotalDiskSpace, gigsDiskSpaceAvailable, percentDiskSpaceAvailable float64
|
||||
if device.MemoryInfo.TotalInternalStorage > 0 {
|
||||
totalStorageBytes := device.MemoryInfo.TotalInternalStorage
|
||||
|
||||
var totalAvailableBytes int64
|
||||
for _, event := range device.MemoryEvents {
|
||||
switch event.EventType {
|
||||
case "EXTERNAL_STORAGE_DETECTED":
|
||||
totalStorageBytes += event.ByteCount
|
||||
case "INTERNAL_STORAGE_MEASURED", "EXTERNAL_STORAGE_MEASURED":
|
||||
totalAvailableBytes += event.ByteCount
|
||||
}
|
||||
}
|
||||
|
||||
if totalStorageBytes > 0 {
|
||||
gigsTotalDiskSpace = float64(totalStorageBytes) / (1024 * 1024 * 1024)
|
||||
gigsDiskSpaceAvailable = float64(totalAvailableBytes) / (1024 * 1024 * 1024)
|
||||
percentDiskSpaceAvailable = (float64(totalAvailableBytes) / float64(totalStorageBytes)) * 100
|
||||
}
|
||||
}
|
||||
|
||||
host := &fleet.AndroidHost{
|
||||
Host: &fleet.Host{
|
||||
TeamID: enrollSecret.GetTeamID(),
|
||||
ComputerName: svc.getComputerName(device),
|
||||
Hostname: svc.getComputerName(device),
|
||||
Platform: "android",
|
||||
OSVersion: "Android " + device.SoftwareInfo.AndroidVersion,
|
||||
Build: device.SoftwareInfo.AndroidBuildNumber,
|
||||
Memory: device.MemoryInfo.TotalRam,
|
||||
HardwareSerial: device.HardwareInfo.SerialNumber,
|
||||
CPUType: device.HardwareInfo.Hardware,
|
||||
HardwareModel: svc.getComputerName(device),
|
||||
HardwareVendor: device.HardwareInfo.Brand,
|
||||
LabelUpdatedAt: time.Time{},
|
||||
DetailUpdatedAt: time.Time{},
|
||||
UUID: device.HardwareInfo.EnterpriseSpecificId,
|
||||
TeamID: enrollSecret.GetTeamID(),
|
||||
ComputerName: svc.getComputerName(device),
|
||||
Hostname: svc.getComputerName(device),
|
||||
Platform: "android",
|
||||
OSVersion: "Android " + device.SoftwareInfo.AndroidVersion,
|
||||
Build: device.SoftwareInfo.AndroidBuildNumber,
|
||||
Memory: device.MemoryInfo.TotalRam,
|
||||
GigsTotalDiskSpace: gigsTotalDiskSpace,
|
||||
GigsDiskSpaceAvailable: gigsDiskSpaceAvailable,
|
||||
PercentDiskSpaceAvailable: percentDiskSpaceAvailable,
|
||||
HardwareSerial: device.HardwareInfo.SerialNumber,
|
||||
CPUType: device.HardwareInfo.Hardware,
|
||||
HardwareModel: svc.getComputerName(device),
|
||||
HardwareVendor: device.HardwareInfo.Brand,
|
||||
LabelUpdatedAt: time.Time{},
|
||||
DetailUpdatedAt: time.Time{},
|
||||
UUID: device.HardwareInfo.EnterpriseSpecificId,
|
||||
},
|
||||
Device: &android.Device{
|
||||
DeviceID: deviceID,
|
||||
|
|
|
|||
|
|
@ -193,6 +193,52 @@ func TestPubSubEnrollment(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestAndroidStorageExtraction(t *testing.T) {
|
||||
svc, mockDS := createAndroidService(t)
|
||||
|
||||
mockDS.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
|
||||
return &fleet.AppConfig{
|
||||
MDM: fleet.MDM{AndroidEnabledAndConfigured: true},
|
||||
}, nil
|
||||
}
|
||||
|
||||
mockDS.AndroidHostLiteFunc = func(ctx context.Context, enterpriseSpecificID string) (*fleet.AndroidHost, error) {
|
||||
return nil, common_mysql.NotFound("android host lite mock")
|
||||
}
|
||||
|
||||
mockDS.VerifyEnrollSecretFunc = func(ctx context.Context, secret string) (*fleet.EnrollSecret, error) {
|
||||
return &fleet.EnrollSecret{Secret: "global"}, nil
|
||||
}
|
||||
|
||||
var createdHost *fleet.AndroidHost
|
||||
mockDS.NewAndroidHostFunc = func(ctx context.Context, host *fleet.AndroidHost) (*fleet.AndroidHost, error) {
|
||||
createdHost = host
|
||||
return host, nil
|
||||
}
|
||||
|
||||
t.Run("extracts storage data from AMAPI device", func(t *testing.T) {
|
||||
createdHost = nil // Reset
|
||||
|
||||
enrollmentMessage := createEnrollmentMessage(t, androidmanagement.Device{
|
||||
Name: createAndroidDeviceId("storage-test"),
|
||||
EnrollmentTokenData: `{"enroll_secret": "global"}`,
|
||||
})
|
||||
|
||||
err := svc.ProcessPubSubPush(context.Background(), "value", enrollmentMessage)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, createdHost)
|
||||
require.NotNil(t, createdHost.Host)
|
||||
|
||||
// Total: 64GB (internal) + 64GB (external) = 128GB
|
||||
// Available: 10GB (internal free) + 25GB (external free) = 35GB
|
||||
// Percentage: 35/128 * 100 = 27.34%
|
||||
require.Equal(t, 128.0, createdHost.Host.GigsTotalDiskSpace, "should calculate total storage (64GB internal + 64GB external)")
|
||||
require.Equal(t, 35.0, createdHost.Host.GigsDiskSpaceAvailable, "should calculate total available storage (10GB + 25GB)")
|
||||
require.InDelta(t, 27.34, createdHost.Host.PercentDiskSpaceAvailable, 0.1, "should calculate percentage (35/128*100=27.34%)")
|
||||
})
|
||||
}
|
||||
|
||||
func createEnrollmentMessage(t *testing.T, deviceInfo androidmanagement.Device) *android.PubSubMessage {
|
||||
deviceInfo.HardwareInfo = &androidmanagement.HardwareInfo{
|
||||
EnterpriseSpecificId: strings.ToUpper(uuid.New().String()),
|
||||
|
|
@ -206,7 +252,26 @@ func createEnrollmentMessage(t *testing.T, deviceInfo androidmanagement.Device)
|
|||
AndroidVersion: "1",
|
||||
}
|
||||
deviceInfo.MemoryInfo = &androidmanagement.MemoryInfo{
|
||||
TotalRam: 1000,
|
||||
TotalRam: int64(8 * 1024 * 1024 * 1024), // 8GB RAM in bytes
|
||||
TotalInternalStorage: int64(64 * 1024 * 1024 * 1024), // 64GB system partition
|
||||
}
|
||||
|
||||
deviceInfo.MemoryEvents = []*androidmanagement.MemoryEvent{
|
||||
{
|
||||
EventType: "EXTERNAL_STORAGE_DETECTED",
|
||||
ByteCount: int64(64 * 1024 * 1024 * 1024), // 64GB external/built-in storage total capacity
|
||||
CreateTime: "2024-01-15T09:00:00Z",
|
||||
},
|
||||
{
|
||||
EventType: "INTERNAL_STORAGE_MEASURED",
|
||||
ByteCount: int64(10 * 1024 * 1024 * 1024), // 10GB free in system partition
|
||||
CreateTime: "2024-01-15T10:00:00Z",
|
||||
},
|
||||
{
|
||||
EventType: "EXTERNAL_STORAGE_MEASURED",
|
||||
ByteCount: int64(25 * 1024 * 1024 * 1024), // 25GB free in external/built-in storage
|
||||
CreateTime: "2024-01-15T10:00:00Z",
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(deviceInfo)
|
||||
|
|
|
|||
|
|
@ -13713,6 +13713,50 @@ func (s *integrationTestSuite) TestListAndroidHostsInLabel() {
|
|||
require.Equal(t, len(hostIDs), countResp.Count)
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestAndroidHostStorageInAPI() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
||||
// Android host with storage data
|
||||
hostID := createAndroidHostWithStorage(t, s.ds, nil)
|
||||
|
||||
// individual host endpoint
|
||||
var hostResp getHostResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", hostID), nil, http.StatusOK, &hostResp)
|
||||
|
||||
require.NotNil(t, hostResp.Host)
|
||||
require.Equal(t, "android", hostResp.Host.Platform)
|
||||
|
||||
// storage data is present in API response
|
||||
assert.Equal(t, 128.0, hostResp.Host.GigsTotalDiskSpace, "API should return total disk space")
|
||||
assert.Equal(t, 35.0, hostResp.Host.GigsDiskSpaceAvailable, "API should return available disk space")
|
||||
assert.InDelta(t, 27.34, hostResp.Host.PercentDiskSpaceAvailable, 0.1, "API should return disk space percentage")
|
||||
|
||||
// list endpoint includes storage data
|
||||
var listResp listHostsResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/hosts", nil, http.StatusOK, &listResp)
|
||||
|
||||
var androidHost *fleet.HostResponse
|
||||
for _, host := range listResp.Hosts {
|
||||
if host.ID == hostID {
|
||||
androidHost = &host
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
require.NotNil(t, androidHost, "Android host should be in hosts list")
|
||||
require.Equal(t, "android", androidHost.Platform)
|
||||
|
||||
// storage data in list endpoint
|
||||
assert.Equal(t, 128.0, androidHost.GigsTotalDiskSpace, "Host list should include total disk space")
|
||||
assert.Equal(t, 35.0, androidHost.GigsDiskSpaceAvailable, "Host list should include available disk space")
|
||||
assert.InDelta(t, 27.34, androidHost.PercentDiskSpaceAvailable, 0.1, "Host list should include disk space percentage")
|
||||
|
||||
// clean up
|
||||
err := s.ds.DeleteHost(ctx, hostID)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func createAndroidHosts(t *testing.T, ds *mysql.Datastore, count int, teamID *uint) []uint {
|
||||
ids := make([]uint, 0, count)
|
||||
for i := range count {
|
||||
|
|
@ -13742,6 +13786,34 @@ func createAndroidHosts(t *testing.T, ds *mysql.Datastore, count int, teamID *ui
|
|||
return ids
|
||||
}
|
||||
|
||||
func createAndroidHostWithStorage(t *testing.T, ds *mysql.Datastore, teamID *uint) uint {
|
||||
host := &fleet.AndroidHost{
|
||||
Host: &fleet.Host{
|
||||
Hostname: "android-storage-host",
|
||||
ComputerName: "Android Storage Test Device",
|
||||
Platform: "android",
|
||||
OSVersion: "Android 14",
|
||||
Build: "UPB4.230623.005",
|
||||
Memory: 8192, // 8GB RAM
|
||||
TeamID: teamID,
|
||||
HardwareSerial: "STORAGE-TEST-" + uuid.NewString(),
|
||||
GigsTotalDiskSpace: 128.0, // 64GB system + 64GB external
|
||||
GigsDiskSpaceAvailable: 35.0, // 10GB + 25GB available
|
||||
PercentDiskSpaceAvailable: 27.34, // 35/128 * 100
|
||||
},
|
||||
Device: &android.Device{
|
||||
DeviceID: strings.ReplaceAll(uuid.NewString(), "-", ""),
|
||||
EnterpriseSpecificID: ptr.String(uuid.NewString()),
|
||||
AndroidPolicyID: ptr.Uint(1),
|
||||
LastPolicySyncTime: ptr.Time(time.Now().Add(-time.Hour)),
|
||||
},
|
||||
}
|
||||
host.SetNodeKey(*host.Device.EnterpriseSpecificID)
|
||||
ahost, err := ds.NewAndroidHost(context.Background(), host)
|
||||
require.NoError(t, err)
|
||||
return ahost.Host.ID
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestHostCertificates() {
|
||||
t := s.T()
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
Loading…
Reference in a new issue