Skip to content

Commit 32aa102

Browse files
committed
Things mostly working.
1 parent 6aedade commit 32aa102

File tree

9 files changed

+151
-5
lines changed

9 files changed

+151
-5
lines changed

server/android/android.go

+6
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,9 @@ func (e Enterprise) Name() string {
1818
type EnrollmentToken struct {
1919
Value string `json:"value"`
2020
}
21+
22+
type Host struct {
23+
HostID uint `db:"host_id"`
24+
FleetEnterpriseID uint `db:"enterprise_id"`
25+
DeviceID string `db:"device_id"`
26+
}

server/android/datastore.go

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ type Datastore interface {
1414
GetEnterpriseByID(ctx context.Context, ID uint) (*Enterprise, error)
1515
UpdateEnterprise(ctx context.Context, enterprise *Enterprise) error
1616
ListEnterprises(ctx context.Context) ([]*Enterprise, error)
17+
18+
AddHost(ctx context.Context, host *Host) error
19+
GetHost(ctx context.Context, fleetEnterpriseID uint, deviceID string) (*Host, error)
1720
}
1821

1922
type MigrationStatus struct {

server/android/job/job.go

+70-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,16 @@ package job
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"os"
8+
"strings"
9+
"time"
710

811
"github.com/fleetdm/fleet/v4/server/android"
912
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
1013
"github.com/fleetdm/fleet/v4/server/fleet"
1114
kitlog "github.com/go-kit/log"
15+
"github.com/go-kit/log/level"
1216
"google.golang.org/api/androidmanagement/v1"
1317
"google.golang.org/api/option"
1418
)
@@ -36,16 +40,80 @@ func ReconcileDevices(ctx context.Context, ds fleet.Datastore, androidDS android
3640

3741
for _, enterprise := range enterprises {
3842
// Note: we can optimize this by using Fields to retrieve partial data https://developers.google.com/gdata/docs/2.0/basics#PartialResponse
43+
// But actually this is not scalable for 100,000s devices, so we need to use PubSub.
3944
devices, err := mgmt.Enterprises.Devices.List(enterprise.Name()).Do()
4045
if err != nil {
4146
return ctxerr.Wrap(ctx, err, "listing devices with Google API")
4247
}
4348

4449
for _, device := range devices.Devices {
45-
logger.Log("msg", "device", "device", device)
50+
// Get the deviceId from the name: enterprises/{enterpriseId}/devices/{deviceId}
51+
nameParts := strings.Split(device.Name, "/")
52+
if len(nameParts) != 4 {
53+
return ctxerr.Errorf(ctx, "invalid Android device name: %s", device.Name)
54+
}
55+
deviceID := nameParts[3]
56+
57+
host, err := androidDS.GetHost(ctx, enterprise.ID, deviceID)
58+
if err != nil {
59+
return ctxerr.Wrap(ctx, err, "getting host")
60+
}
61+
if host != nil {
62+
// TODO: Update host if needed
63+
continue
64+
}
65+
66+
// TODO: Do EnrollHost and androidDS.AddHost inside a transaction so we don't add duplicate hosts
67+
fleetHost, err := ds.EnrollHost(ctx, true, device.HardwareInfo.SerialNumber, device.HardwareInfo.SerialNumber,
68+
device.HardwareInfo.SerialNumber, "", nil, 0)
69+
if err != nil {
70+
return ctxerr.Wrap(ctx, err, "enrolling host")
71+
}
72+
err = androidDS.AddHost(ctx, &android.Host{
73+
FleetEnterpriseID: enterprise.ID,
74+
DeviceID: deviceID,
75+
HostID: fleetHost.ID,
76+
})
77+
if err != nil {
78+
return ctxerr.Wrap(ctx, err, "adding Android host")
79+
}
80+
81+
fleetHost.DiskEncryptionEnabled = &device.DeviceSettings.IsEncrypted
82+
fleetHost.Platform = "ubuntu"
83+
fleetHost.HardwareVendor = device.HardwareInfo.Manufacturer
84+
fleetHost.HardwareModel = device.HardwareInfo.Model
85+
fleetHost.OSVersion = "Android " + device.SoftwareInfo.AndroidVersion
86+
lastEnrolledAt, err := time.Parse(time.RFC3339, device.EnrollmentTime)
87+
switch {
88+
case err != nil:
89+
level.Error(logger).Log("msg", "parsing Android device last enrolled at", "err", err, "deviceId", deviceID)
90+
default:
91+
fleetHost.LastEnrolledAt = lastEnrolledAt
92+
}
93+
detailUpdatedAt, err := time.Parse(time.RFC3339, device.LastStatusReportTime)
94+
switch {
95+
case err != nil:
96+
level.Error(logger).Log("msg", "parsing Android device detail updated at", "err", err, "deviceId", deviceID)
97+
default:
98+
fleetHost.DetailUpdatedAt = detailUpdatedAt
99+
}
100+
err = ds.UpdateHost(ctx, fleetHost)
101+
if err != nil {
102+
return ctxerr.Wrap(ctx, err, fmt.Sprintf("updating host with deviceId %s", deviceID))
103+
}
104+
105+
err = ds.UpdateHostOperatingSystem(ctx, fleetHost.ID, fleet.OperatingSystem{
106+
Name: "Android",
107+
Version: device.SoftwareInfo.AndroidVersion,
108+
Platform: "android",
109+
KernelVersion: device.SoftwareInfo.DeviceKernelVersion,
110+
})
111+
if err != nil {
112+
return ctxerr.Wrap(ctx, err, fmt.Sprintf("updating host operating system with deviceId %s", deviceID))
113+
}
114+
46115
}
47116

48-
// For each device, check whether it is in Fleet. If not, add it
49117
}
50118

51119
return nil
File renamed without changes.

server/android/mysql/hosts.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package mysql
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
8+
"github.com/fleetdm/fleet/v4/server/android"
9+
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
10+
"github.com/jmoiron/sqlx"
11+
)
12+
13+
func (ds *Datastore) GetHost(ctx context.Context, fleetEnterpriseID uint, deviceID string) (*android.Host, error) {
14+
stmt := `SELECT enterprise_id, device_id, host_id FROM android_hosts WHERE enterprise_id = ? AND device_id = ?`
15+
var host android.Host
16+
err := sqlx.GetContext(ctx, ds.reader(ctx), &host, stmt, fleetEnterpriseID, deviceID)
17+
switch {
18+
case errors.Is(err, sql.ErrNoRows):
19+
return nil, nil
20+
case err != nil:
21+
return nil, ctxerr.Wrap(ctx, err, "getting host")
22+
}
23+
return &host, nil
24+
}
25+
26+
func (ds *Datastore) AddHost(ctx context.Context, host *android.Host) error {
27+
stmt := `INSERT INTO android_hosts (enterprise_id, device_id, host_id) VALUES (?, ?, ?)`
28+
_, err := ds.writer(ctx).ExecContext(ctx, stmt, host.FleetEnterpriseID, host.DeviceID, host.HostID)
29+
return ctxerr.Wrap(ctx, err, "adding host")
30+
}

server/android/mysql/migrations/20250101000000_CreateAndroidTables.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,29 @@ func init() {
1010
}
1111

1212
func Up_20250101000000(tx *sql.Tx) error {
13-
_, err := tx.Exec(`CREATE TABLE android_enterprises (
13+
_, err := tx.Exec(`CREATE TABLE IF NOT EXISTS android_enterprises (
1414
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
15-
signup_name VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
16-
enterprise_id VARCHAR(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
15+
signup_name VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
16+
enterprise_id VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
1717
created_at DATETIME(6) NULL DEFAULT NOW(6),
1818
updated_at DATETIME(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6))`)
1919
if err != nil {
2020
return fmt.Errorf("failed to create android_enterprise table: %w", err)
2121
}
22+
23+
_, err = tx.Exec(`CREATE TABLE IF NOT EXISTS android_hosts (
24+
id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
25+
host_id INT UNSIGNED NULL,
26+
enterprise_id INT UNSIGNED NOT NULL,
27+
device_id VARCHAR(63) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
28+
created_at DATETIME(6) NULL DEFAULT NOW(6),
29+
updated_at DATETIME(6) NULL DEFAULT NOW(6) ON UPDATE NOW(6),
30+
UNIQUE KEY idx_android_hosts_enterprise_device (enterprise_id, device_id) -- consider making this the primary key
31+
)`)
32+
if err != nil {
33+
return fmt.Errorf("failed to create android_enterprise table: %w", err)
34+
}
35+
2236
logger.Info.Println("Done with initial migration.")
2337
return nil
2438
}

server/datastore/mysql/operating_systems.go

+5
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ func (ds *Datastore) UpdateHostOperatingSystem(ctx context.Context, hostID uint,
4646
if !updateNeeded {
4747
return nil
4848
}
49+
50+
const maxDisplayVersionLength = 10 // per DB schema
51+
if len(hostOS.DisplayVersion) > maxDisplayVersionLength {
52+
return ctxerr.Errorf(ctx, "host OS display version too long: %s", hostOS.DisplayVersion)
53+
}
4954
return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
5055
os, err := getOrGenerateOperatingSystemDB(ctx, tx, hostOS)
5156
if err != nil {

tools/android/android.go

+20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func main() {
2424

2525
command := flag.String("command", "", "")
2626
enterpriseID := flag.String("enterprise_id", "", "")
27+
deviceID := flag.String("device_id", "", "")
2728
flag.Parse()
2829

2930
ctx := context.Background()
@@ -41,6 +42,8 @@ func main() {
4142
policiesList(mgmt, *enterpriseID)
4243
case "devices.list":
4344
devicesList(mgmt, *enterpriseID)
45+
case "devices.issueCommand.RELINQUISH_OWNERSHIP":
46+
devicesRelinquishOwnership(mgmt, *enterpriseID, *deviceID)
4447
default:
4548
log.Fatalf("Unknown command: %s", *command)
4649
}
@@ -108,3 +111,20 @@ func devicesList(mgmt *androidmanagement.Service, enterpriseID string) {
108111
log.Println(string(data))
109112
}
110113
}
114+
115+
func devicesRelinquishOwnership(mgmt *androidmanagement.Service, enterpriseID, deviceID string) {
116+
if enterpriseID == "" || deviceID == "" {
117+
log.Fatalf("enterprise_id and device_id must be set")
118+
}
119+
operation, err := mgmt.Enterprises.Devices.IssueCommand("enterprises/"+enterpriseID+"/devices/"+deviceID, &androidmanagement.Command{
120+
Type: "RELINQUISH_OWNERSHIP",
121+
}).Do()
122+
if err != nil {
123+
log.Fatalf("Error issuing command: %v", err)
124+
}
125+
data, err := json.MarshalIndent(operation, "", " ")
126+
if err != nil {
127+
log.Fatalf("Error marshalling operation: %v", err)
128+
}
129+
log.Println(string(data))
130+
}

0 commit comments

Comments
 (0)