Proposal fix/plan for 24024 (#24207)

This commit is contained in:
Martin Angers 2024-11-27 12:11:08 -05:00 committed by GitHub
parent 24f83d880a
commit eea90e5632
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 181 additions and 61 deletions

View file

@ -0,0 +1,2 @@
* Bypass the setup experience UI if there is no setup experience item to process (no software to install, no script to execute), so that releasing the device is done without going through that window.
* Fixed releasing a DEP-enrolled macOS device if mTLS is configured for `fleetd`.

View file

@ -32,13 +32,14 @@ const (
// Not all options are required for all actions, each individual action should
// validate that it receives the required information.
type HostOptions struct {
Action HostAction
Platform string
UUID string
HardwareSerial string
HardwareModel string
EnrollReference string
Host *fleet.Host
Action HostAction
Platform string
UUID string
HardwareSerial string
HardwareModel string
EnrollReference string
Host *fleet.Host
HasSetupExperienceItems bool
}
// HostLifecycle manages MDM host lifecycle actions
@ -174,6 +175,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
opts.Platform,
tmID,
opts.EnrollReference,
!opts.HasSetupExperienceItems,
)
return ctxerr.Wrap(ctx, err, "queue DEP post-enroll task")
}
@ -189,6 +191,7 @@ func (t *HostLifecycle) turnOnDarwin(ctx context.Context, opts HostOptions) erro
opts.Platform,
tmID,
opts.EnrollReference,
false,
); err != nil {
return ctxerr.Wrap(ctx, err, "queue manual post-enroll task")
}

View file

@ -2778,20 +2778,21 @@ func (svc *MDMAppleCheckinAndCommandService) TokenUpdate(r *mdm.Request, m *mdm.
return ctxerr.Wrap(r.Context, err, "cleaning SCEP refs")
}
var hasSetupExpItems bool
if m.AwaitingConfiguration {
// Enqueue setup experience items and mark the host as being in setup experience
_, err := svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID)
hasSetupExpItems, err = svc.ds.EnqueueSetupExperienceItems(r.Context, r.ID, info.TeamID)
if err != nil {
return ctxerr.Wrap(r.Context, err, "queueing setup experience tasks")
}
}
return svc.mdmLifecycle.Do(r.Context, mdmlifecycle.HostOptions{
Action: mdmlifecycle.HostActionTurnOn,
Platform: info.Platform,
UUID: r.ID,
EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey],
Action: mdmlifecycle.HostActionTurnOn,
Platform: info.Platform,
UUID: r.ID,
EnrollReference: r.Params[mobileconfig.FleetEnrollReferenceKey],
HasSetupExperienceItems: hasSetupExpItems,
})
}

View file

@ -121,12 +121,33 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceGlobal() {
s.enableABM("fleet_ade_test")
// add a setup experience script to run for no team
extraArgs := make(map[string][]string)
body, headers := generateNewScriptMultipartRequest(t,
"script.sh", []byte(`echo "hello"`), s.token, extraArgs)
s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers)
// test manual and automatic release with the new setup experience flow
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false)
})
}
// test manual and automatic release with the old worker flow
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", true)
})
}
// remove the setup experience script, run the new setup experience flow when
// there is no setup experience item to process (so it is bypassed)
s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK)
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, globalDevice, enableReleaseManually, nil, "I1", false)
})
}
}
func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() {
@ -211,12 +232,35 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseDeviceTeam() {
// enable FileVault
s.Do("PATCH", "/api/latest/fleet/mdm/apple/settings", json.RawMessage([]byte(fmt.Sprintf(`{"enable_disk_encryption":true,"team_id":%d}`, tm.ID))), http.StatusNoContent)
// add a setup experience script to run for this team
extraArgs := map[string][]string{
"team_id": {fmt.Sprintf("%d", tm.ID)},
}
body, headers := generateNewScriptMultipartRequest(t,
"script.sh", []byte(`echo "hello"`), s.token, extraArgs)
s.DoRawWithHeaders("POST", "/api/latest/fleet/setup_experience/script", body.Bytes(), http.StatusOK, headers)
// test manual and automatic release with the new setup experience flow
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t;new_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false)
})
}
// test manual and automatic release with the old worker flow
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t", enableReleaseManually), func(t *testing.T) {
t.Run(fmt.Sprintf("enableReleaseManually=%t;old_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", true)
})
}
// remove the setup experience script, run the new setup experience flow when
// there is no setup experience item to process (so it is bypassed)
s.Do("DELETE", "/api/latest/fleet/setup_experience/script", nil, http.StatusOK, "team_id", fmt.Sprint(tm.ID))
for _, enableReleaseManually := range []bool{false, true} {
t.Run(fmt.Sprintf("enableReleaseManually=%t;bypass_flow", enableReleaseManually), func(t *testing.T) {
s.runDEPEnrollReleaseDeviceTest(t, teamDevice, enableReleaseManually, &tm.ID, "I2", false)
})
}
}
func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() {
@ -286,6 +330,11 @@ func (s *integrationMDMTestSuite) TestDEPEnrollReleaseIphoneTeam() {
func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, device godep.Device, enableReleaseManually bool, teamID *uint, customProfileIdent string, useOldFleetdFlow bool) {
ctx := context.Background()
var isIphone bool
if device.DeviceFamily == "iPhone" {
isIphone = true
}
// set the enable release device manually option
payload := map[string]any{
"enable_release_device_manually": enableReleaseManually,
@ -359,15 +408,22 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
// enroll the host
depURLToken := loadEnrollmentProfileDEPToken(t, s.ds)
mdmDevice := mdmtest.NewTestMDMClientAppleDEP(s.server.URL, depURLToken)
var isIphone bool
if device.DeviceFamily == "iPhone" {
if isIphone {
mdmDevice.Model = "iPhone 14,6"
isIphone = true
}
mdmDevice.SerialNumber = device.SerialNumber
err := mdmDevice.Enroll()
require.NoError(t, err)
// check if it has setup experience items or not
hasSetupExpItems := true
_, err = s.ds.GetHostAwaitingConfiguration(ctx, mdmDevice.UUID)
if fleet.IsNotFound(err) {
hasSetupExpItems = false
} else if err != nil {
require.NoError(t, err)
}
// run the worker to process the DEP enroll request
s.runWorker()
// run the cron to assign configuration profiles
@ -525,8 +581,13 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
b, err := io.ReadAll(res.Body)
require.NoError(t, err)
require.NoError(t, json.Unmarshal(b, &orbitConfigResp))
// should be notified of the setup experience flow
require.False(t, orbitConfigResp.Notifications.RunSetupExperience)
if hasSetupExpItems {
// should be notified of the setup experience flow
require.True(t, orbitConfigResp.Notifications.RunSetupExperience)
} else {
// should bypass the setup experience flow
require.False(t, orbitConfigResp.Notifications.RunSetupExperience)
}
if enableReleaseManually {
// get the worker's pending job from the future, there should not be any
@ -537,7 +598,7 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
return
}
if useOldFleetdFlow {
if useOldFleetdFlow || !hasSetupExpItems {
// there should be a Release Device pending job
pending, err := s.ds.GetQueuedJobs(ctx, 2, time.Now().UTC().Add(time.Minute))
require.NoError(t, err)
@ -574,6 +635,12 @@ func (s *integrationMDMTestSuite) runDEPEnrollReleaseDeviceTest(t *testing.T, de
require.NoError(t, err)
require.Len(t, pending, 0)
// mark the setup experience script as done
mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
_, err := q.ExecContext(ctx, `UPDATE setup_experience_status_results SET status = 'success' WHERE host_uuid = ?`, mdmDevice.UUID)
return err
})
// call the /status endpoint to automatically release the host
var statusResp getOrbitSetupExperienceStatusResponse
s.DoJSON("POST", "/api/fleet/orbit/setup_experience/status", json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q}`, *enrolledHost.OrbitNodeKey)), http.StatusOK, &statusResp)

View file

@ -677,6 +677,14 @@ func (s *integrationMDMTestSuite) TearDownTest() {
_, err := tx.ExecContext(ctx, "DELETE FROM vpp_tokens;")
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_status_results;")
return err
})
mysql.ExecAdhocSQL(t, s.ds, func(tx sqlx.ExtContext) error {
_, err := tx.ExecContext(ctx, "DELETE FROM setup_experience_scripts;")
return err
})
}
func (s *integrationMDMTestSuite) mockDEPResponse(orgName string, handler http.Handler) {

View file

@ -249,15 +249,13 @@ func (svc *Service) GetOrbitConfig(ctx context.Context) (fleet.OrbitConfig, erro
notifs.RunSetupExperience = true
}
if inSetupAssistant || fleet.IsNotFound(err) {
// If the client is running a fleetd that doesn't support setup experience, or if no
// software/script has been configured for setup experience, then we should fall back to
// the "old way" of releasing the device. We do an additional check for
// !inSetupAssistant to prevent enqueuing a new job every time the /config
// endpoint is hit.
if inSetupAssistant {
// If the client is running a fleetd that doesn't support setup
// experience, then we should fall back to the "old way" of releasing
// the device.
mp, ok := capabilities.FromContext(ctx)
if !ok || !mp.Has(fleet.CapabilitySetupExperience) || !inSetupAssistant {
level.Debug(svc.logger).Log("msg", "host doesn't support setup experience or no setup experience configured, falling back to worker-based device release", "host_uuid", host.UUID)
if !ok || !mp.Has(fleet.CapabilitySetupExperience) {
level.Debug(svc.logger).Log("msg", "host doesn't support setup experience, falling back to worker-based device release", "host_uuid", host.UUID)
if err := svc.processReleaseDeviceForOldFleetd(ctx, host); err != nil {
return fleet.OrbitConfig{}, err
}
@ -521,7 +519,7 @@ func (svc *Service) processReleaseDeviceForOldFleetd(ctx context.Context, host *
// Enroll reference arg is not used in the release device task, passing empty string.
if err := worker.QueueAppleMDMJob(ctx, svc.ds, svc.logger, worker.AppleMDMPostDEPReleaseDeviceTask,
host.UUID, host.Platform, host.TeamID, "", bootstrapCmdUUID, acctConfigCmdUUID); err != nil {
host.UUID, host.Platform, host.TeamID, "", false, bootstrapCmdUUID, acctConfigCmdUUID); err != nil {
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
}
}

View file

@ -50,12 +50,13 @@ func (a *AppleMDM) Name() string {
// appleMDMArgs is the payload for the Apple MDM job.
type appleMDMArgs struct {
Task AppleMDMTask `json:"task"`
HostUUID string `json:"host_uuid"`
TeamID *uint `json:"team_id,omitempty"`
EnrollReference string `json:"enroll_reference,omitempty"`
EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
Platform string `json:"platform,omitempty"`
Task AppleMDMTask `json:"task"`
HostUUID string `json:"host_uuid"`
TeamID *uint `json:"team_id,omitempty"`
EnrollReference string `json:"enroll_reference,omitempty"`
EnrollmentCommands []string `json:"enrollment_commands,omitempty"`
Platform string `json:"platform,omitempty"`
UseWorkerDeviceRelease bool `json:"use_worker_device_release,omitempty"`
}
// Run executes the apple_mdm job.
@ -163,9 +164,10 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
}
}
// proceed to release the device only if it is not a macos, as those are
// released via the setup experience flow.
if !isMacOS(args.Platform) {
// proceed to release the device if it is not a macos, as those are released
// via the setup experience flow, or if we were told to use the worker based
// release.
if !isMacOS(args.Platform) || args.UseWorkerDeviceRelease {
var manualRelease bool
if args.TeamID == nil {
ac, err := a.Datastore.AppConfig(ctx)
@ -187,7 +189,7 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
// be final and same for MDM profiles of that host; it means the DEP
// enrollment process is done and the device can be released.
if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, awaitCmdUUIDs...); err != nil {
args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, false, awaitCmdUUIDs...); err != nil {
return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
}
}
@ -198,10 +200,11 @@ func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs)
// This job is deprecated for macos because releasing devices is now done via
// the orbit endpoint /setup_experience/status that is polled by a swift dialog
// UI window during the setup process, and automatically releases the device
// once all pending setup tasks are done. However, it must remain implemented
// for iOS and iPadOS and in case there are such jobs to process after a Fleet
// migration to a new version.
// UI window during the setup process (unless there are no setup experience
// items, in which case this worker job is used), and automatically releases
// the device once all pending setup tasks are done. However, it must remain
// implemented for iOS and iPadOS and in case there are such jobs to process
// after a Fleet migration to a new version.
func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error {
// Edge cases:
// - if the device goes offline for a long time, should we go ahead and
@ -355,6 +358,7 @@ func QueueAppleMDMJob(
platform string,
teamID *uint,
enrollReference string,
useWorkerDeviceRelease bool,
enrollmentCommandUUIDs ...string,
) error {
attrs := []interface{}{
@ -373,12 +377,13 @@ func QueueAppleMDMJob(
level.Info(logger).Log(attrs...)
args := &appleMDMArgs{
Task: task,
HostUUID: hostUUID,
TeamID: teamID,
EnrollReference: enrollReference,
EnrollmentCommands: enrollmentCommandUUIDs,
Platform: platform,
Task: task,
HostUUID: hostUUID,
TeamID: teamID,
EnrollReference: enrollReference,
EnrollmentCommands: enrollmentCommandUUIDs,
Platform: platform,
UseWorkerDeviceRelease: useWorkerDeviceRelease,
}
// the release device task is always added with a delay

View file

@ -141,7 +141,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should mark the job as done
@ -171,7 +171,7 @@ func TestAppleMDM(t *testing.T) {
// create a host and enqueue the job
h := createEnrolledHost(t, 1, nil, true)
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "")
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMTask("no-such-task"), h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should mark the job as failed
@ -204,7 +204,7 @@ func TestAppleMDM(t *testing.T) {
w.Register(mdmWorker)
// use "" instead of "darwin" as platform to test a queued job after the upgrade to iOS/iPadOS support.
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "")
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -239,7 +239,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -281,7 +281,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "")
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -330,7 +330,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -380,7 +380,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "")
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -418,7 +418,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd")
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "abcd", false)
require.NoError(t, err)
// run the worker, should succeed
@ -461,7 +461,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, idpAcc.UUID, false)
require.NoError(t, err)
// run the worker, should succeed
@ -514,7 +514,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID)
err = QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", &tm.ID, idpAcc.UUID, false)
require.NoError(t, err)
// run the worker, should succeed
@ -548,7 +548,7 @@ func TestAppleMDM(t *testing.T) {
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "")
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostManualEnrollmentTask, h.UUID, "darwin", nil, "", false)
require.NoError(t, err)
// run the worker, should succeed
@ -564,4 +564,40 @@ func TestAppleMDM(t *testing.T) {
require.Empty(t, jobs)
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
})
t.Run("use worker for automatic release", func(t *testing.T) {
mysql.SetTestABMAssets(t, ds, testOrgName)
defer mysql.TruncateTables(t, ds)
h := createEnrolledHost(t, 1, nil, true)
mdmWorker := &AppleMDM{
Datastore: ds,
Log: nopLog,
Commander: apple_mdm.NewMDMAppleCommander(mdmStorage, mockPusher{}),
}
w := NewWorker(ds, nopLog)
w.Register(mdmWorker)
err := QueueAppleMDMJob(ctx, ds, nopLog, AppleMDMPostDEPEnrollmentTask, h.UUID, "darwin", nil, "", true)
require.NoError(t, err)
// run the worker, should succeed
err = w.ProcessJobs(ctx)
require.NoError(t, err)
// ensure the job's not_before allows it to be returned if it were to run
// again
time.Sleep(time.Second)
require.ElementsMatch(t, []string{"InstallEnterpriseApplication"}, getEnqueuedCommandTypes(t))
// the release device job got enqueued
jobs, err := ds.GetQueuedJobs(ctx, 1, time.Now().Add(time.Minute)) // release job is always added with a delay
require.NoError(t, err)
require.Len(t, jobs, 1)
require.Equal(t, fleet.JobStateQueued, jobs[0].State)
require.Equal(t, appleMDMJobName, jobs[0].Name)
require.Contains(t, string(*jobs[0].Args), AppleMDMPostDEPReleaseDeviceTask)
})
}