blob: b4cf29eb89e02522c878ce24d191b56bde1596f6 [file] [log] [blame] [edit]
import {
AfterContentInit,
ChangeDetectorRef,
Component,
OnDestroy,
OnInit,
ViewChild,
} from '@angular/core';
import {MatDialog} from '@angular/material/dialog';
import {FormControl, Validators} from '@angular/forms';
import {leadingTrailingWhitespaceValidator} from '../utils/validators';
import {MoblabGrpcService} from '../services/moblab-grpc.service';
import {MoblabConfigurationGrpcService} from '../services/moblab-configuration-grpc.service';
import {MoblabSettingsService} from '../services/moblab-settings.service';
import {ConfigSetupService} from '../services/moblab-configuration-alerting.service';
import {ButtonWithProgressComponent} from '../widgets/button-with-progress/button-with-progress.component';
import {StageBuildDialogComponent} from '../manage-duts/stage-build-dialog/stage-build-dialog.component';
import {MoblabSettingsConstains, build_access_request_link} from '../constants';
import {NotificationsService} from '../services/notifications.service';
import {Subscription} from 'rxjs';
import {GlobalInfoService} from 'app/services/global-ui-settings.service';
import {BuildTargetStableVersion} from '../services/moblab_configuration_rpc_pb';
import {MatTableDataSource} from '@angular/material/table';
function gsURLValidator(control) {
if (!control.value) {
return null;
}
const forbidden = !new RegExp('^gs:\\/\\/.*').test(control.value);
return forbidden ? {notAGSUrl: {value: control.value}} : null;
}
function validChromeosBuildPattern(control) {
if (!control.value) {
return null;
}
const value = control.value.trim();
const forbidden = !new RegExp(
/^R[0-9]{2,3}-[0-9]{5,}\.[0-9]{1,}\.[0-9]{1,}$/
).test(value);
return forbidden ? {validChromeosBuildPattern: {value: control.value}} : null;
}
class CloudConfiguration {
constructor(
public isCloudEnabled: boolean = false,
public isRemoteConsoleEnabled: boolean = false,
public isRemoteCommandEnabled: boolean = false,
public botoKeyID: string = '',
public botoKeySecret: string = '',
public bucketURL: string = ''
) {}
}
@Component({
selector: 'app-configuration',
templateUrl: './configuration.component.html',
styleUrls: ['./configuration.component.scss'],
})
export class ConfigurationComponent
implements OnInit, OnDestroy, AfterContentInit
{
@ViewChild('pauseButton') pauseButton: ButtonWithProgressComponent;
@ViewChild('resumeButton') resumeButton: ButtonWithProgressComponent;
readonly build_access_request_link = build_access_request_link;
errors = false;
canUpdateCloudConfig = false;
isCloudEnabled = false;
isRemoteConsoleEnabled = false;
isRemoteCommandEnabled = false;
isHostSchedulerPauseRequested = false;
botoKeyIDForm = new FormControl('', {
validators: [Validators.required],
});
botoKeySecretForm = new FormControl('', {
validators: [Validators.required],
});
bucketURLForm = new FormControl('', {
validators: [Validators.required, gsURLValidator],
});
userCrOSVersionForm = new FormControl('', {
validators: [validChromeosBuildPattern],
});
resultsRetentionPeriodForm = new FormControl('', [
Validators.required,
Validators.pattern(/^(([01]?[1-9]|2[0-3])h)$|^([1-7]d)$/),
]);
userCrOSVersionValue = '';
resultsRetentionPeriod = '';
bucketLink = '';
cloudConfigDisabledMessage = '';
showUncheckWarning = false;
configurationLoading = false;
lastFetchedCloudConfig = new CloudConfiguration();
dutWifiInfoLoading = false;
dutWifiNameForm = new FormControl('', {
validators: [leadingTrailingWhitespaceValidator],
});
dutWifiPasswordForm = new FormControl('');
lastFetchedDutWifiName = '';
lastFetchedDutWifiPassword = '';
pauseStatusSubscription: Subscription | null = null;
panelOpenState = true;
stableBuildVersionLoading = false;
stableBuildVersions: MatTableDataSource<BuildTargetStableVersion> =
new MatTableDataSource<BuildTargetStableVersion>([]);
displayedColumns: string[] = [
'build_target',
'default_version',
'override_version',
];
constructor(
private changeDetector: ChangeDetectorRef,
private moblabRpcService: MoblabGrpcService,
private settingsService: MoblabSettingsService,
private moblabConfigurationRpcService: MoblabConfigurationGrpcService,
private configSetupService: ConfigSetupService,
private notificationsService: NotificationsService,
private globalInfo: GlobalInfoService,
private dialog: MatDialog
) {}
async ngOnInit(): Promise<void> {
this.bucketURLForm.valueChanges.subscribe(value => {
this.formatBucketLink();
});
this.pauseStatusSubscription =
this.settingsService.pauseRequestorsObservable.subscribe(requestors => {
this.isHostSchedulerPauseRequested =
requestors &&
requestors.includes(MoblabSettingsConstains.USER_REQUEST_STRING);
});
this.getCloudConfiguration();
this.getDutWifiInfo();
this.initCanUpdateCloudConfigFlag();
await Promise.all([
this.listStableBuildVersions(),
this.getResultsRetentionPeriod(),
]);
}
ngOnDestroy(): void {
this.pauseStatusSubscription?.unsubscribe();
}
async listStableBuildVersions() {
console.log(this.stableBuildVersions);
this.userCrOSVersionForm.disable({emitEvent: false});
this.stableBuildVersionLoading = true;
console.log(this.stableBuildVersions);
try {
const data =
await this.moblabConfigurationRpcService.listStableBuildVersions();
this.stableBuildVersions = new MatTableDataSource(data);
// User override stable build version is the same for all build targets
if (data.length !== 0) {
this.userCrOSVersionValue = data[0]
.getStableVersion()
.getOverrideVersion();
}
} catch (error) {
console.log('listStableBuildVersions call failed with:' + error);
if (typeof error === 'object' && 'message' in error) {
this.notificationsService.error(
'Failed to fetch stable build versions: ' + error.message
);
}
} finally {
if (this.isAnyDutEnrolled()) {
this.userCrOSVersionForm.enable({emitEvent: false});
}
this.stableBuildVersionLoading = false;
}
}
getStableBuildVersion(buildVersion: BuildTargetStableVersion): string {
return (
buildVersion.getStableVersion().getOverrideVersion() ||
buildVersion.getStableVersion().getDefaultVersion()
);
}
getStableBuildVersionDefault(buildVersion: BuildTargetStableVersion): string {
return buildVersion.getStableVersion().getOverrideVersion().length === 0
? '(default)'
: '';
}
public async getResultsRetentionPeriod() {
this.resultsRetentionPeriodForm.disable();
try {
const period_days =
await this.moblabConfigurationRpcService.getResultsRetentionPeriod();
this.resultsRetentionPeriod = period_days;
this.resultsRetentionPeriodForm.setValue(period_days);
} catch (error) {
console.log('getResultsRetentionPeriod call failed with:' + error);
if (typeof error === 'object' && 'message' in error) {
this.notificationsService.error(
'Failed to fetch results retention duration: ' + error.message
);
}
} finally {
this.resultsRetentionPeriodForm.enable();
}
}
public async setResultsRetentionPeriod(period: string) {
this.resultsRetentionPeriodForm.disable();
try {
await this.moblabConfigurationRpcService.setResultsRetentionPeriod(
period
);
this.getResultsRetentionPeriod();
} catch (error) {
console.log('setStableBuildVersion call failed with:' + error);
if (typeof error === 'object' && 'message' in error) {
this.notificationsService.error(
'Failed to set stable Chrome OS version: ' + error.message
);
}
} finally {
this.resultsRetentionPeriodForm.enable();
}
}
isAnyDutEnrolled = () => this.stableBuildVersions.data.length != 0;
public async setStableCrOSVersion(override_value: string) {
this.stableBuildVersionLoading = true;
this.userCrOSVersionForm.setErrors(null);
try {
await this.moblabConfigurationRpcService.setStableBuildVersion(
override_value
);
await this.listStableBuildVersions();
this.userCrOSVersionForm.reset();
} catch (error) {
console.log('setStableBuildVersion call failed with:' + error);
if (typeof error === 'object' && 'message' in error) {
this.userCrOSVersionForm.setErrors(
{apiResponseError: error.message},
{emitEvent: true}
);
}
} finally {
this.stableBuildVersionLoading = false;
}
}
getCloudConfiguration(calledOnSubmit = false) {
/**
* Method sends RPC command to fetch cloud configuration information and
* apply fetched values to the UI forms.
*
* Parameter calledOnSubmit is set to true when this method is called from
* the onCloudConfigSubmit method.
*/
this.configurationLoading = true;
this.moblabRpcService.get_cloud_configuration(
(
boto_key_id: string,
boto_key_secret: string,
gcs_bucket_url: string,
is_cloud_enabled: boolean,
is_remote_console_enabled: boolean,
is_remote_command_enabled: boolean
) => {
this.updateCloudConfigurationForms(
new CloudConfiguration(
is_cloud_enabled,
is_remote_console_enabled,
is_remote_command_enabled,
boto_key_id,
boto_key_secret,
gcs_bucket_url
)
);
// If called from the onCloudConfigSubmit method, suppress the popup.
if (
!this.configSetupService.getIsCloudConfigEnabled() &&
!calledOnSubmit
) {
this.notificationsService
.notify(`Please enable the Cloud integration to
allow system to fetch Chrome OS test images and upload test
results for CPCon ingestion.`);
}
},
(errorMsg: string) => {
this.configurationLoading = false;
this.notificationsService.error(
'Failed to get cloud configuration: ' + errorMsg
);
}
);
}
getDutWifiInfo() {
this.dutWifiInfoLoading = true;
this.moblabRpcService.get_dut_wifi_info(
(dut_wifi_name: string, dut_wifi_password: string) => {
this.setDutWifiInfo(dut_wifi_name, dut_wifi_password);
},
(errorMsg: string) => {
this.dutWifiInfoLoading = false;
this.notificationsService.error(
'Failed to get DUT wifi info: ' + errorMsg
);
}
);
}
onDutWifiInfoSubmit(formValue) {
this.dutWifiInfoLoading = true;
this.moblabRpcService.set_dut_wifi_info(
(message: string) => {
this.notificationsService.notify(message);
this.getDutWifiInfo();
},
(errorMsg: string) => {
this.dutWifiInfoLoading = false;
this.notificationsService.error(errorMsg);
},
this.dutWifiNameForm.value,
this.dutWifiPasswordForm.value
);
}
isCloudConfigurationChanged() {
/**
* Returns true IFF there is a change in any of the cloud configuration
* values.
*/
return (
this.lastFetchedCloudConfig.botoKeyID !== this.botoKeyIDForm.value ||
this.lastFetchedCloudConfig.botoKeySecret !==
this.botoKeySecretForm.value ||
this.lastFetchedCloudConfig.bucketURL !== this.bucketURLForm.value ||
this.lastFetchedCloudConfig.isCloudEnabled !== this.isCloudEnabled ||
this.lastFetchedCloudConfig.isRemoteConsoleEnabled !==
this.isRemoteConsoleEnabled ||
this.lastFetchedCloudConfig.isRemoteCommandEnabled !==
this.isRemoteCommandEnabled
);
}
isDutWifiInfoValidForSubmit() {
return (
this.dutWifiNameForm.value.length &&
!this.dutWifiNameForm.errors &&
!this.dutWifiPasswordForm.errors &&
(this.lastFetchedDutWifiName !== this.dutWifiNameForm.value ||
this.lastFetchedDutWifiPassword !== this.dutWifiPasswordForm.value)
);
}
isStableCrOsVersionValidForSubmit() {
return (
this.stableBuildVersions.data.length != 0 &&
this.userCrOSVersionForm.value &&
this.userCrOSVersionForm.value.length &&
this.userCrOSVersionForm.value !== this.userCrOSVersionValue &&
!this.userCrOSVersionForm.errors
);
}
isRetentionPeriodValidForSubmit() {
return (
this.resultsRetentionPeriodForm.value !== this.resultsRetentionPeriod &&
!this.resultsRetentionPeriodForm.errors
);
}
private setDutWifiInfo(dut_wifi_name: string, dut_wifi_password: string) {
this.dutWifiNameForm.setValue(dut_wifi_name);
this.dutWifiPasswordForm.setValue(dut_wifi_password);
this.lastFetchedDutWifiName = dut_wifi_name;
this.lastFetchedDutWifiPassword = dut_wifi_password;
this.dutWifiInfoLoading = false;
}
disableCloudConfigSubmissionButton() {
return (
!this.isCloudConfigurationChanged() ||
this.botoKeySecretForm.errors ||
this.botoKeyIDForm.errors ||
this.bucketURLForm.errors
);
}
enableLink() {
return !this.bucketURLForm.errors;
}
private formatBucketLink() {
if (this.enableLink()) {
this.bucketLink =
'https://console.cloud.google.com/storage/browser/' +
this.bucketURLForm.value.split('//')[1];
} else {
this.bucketLink = '';
}
}
toggleCloudConfigDisabledMessage() {
/**
* Will set tooltip hover of cloud configuration forms to show that cloud
* integration needs to be enabled before any change is made.
*/
if (this.isCloudEnabled) {
this.cloudConfigDisabledMessage = '';
} else {
this.cloudConfigDisabledMessage =
"Cloud settings are not enabled, please check 'enable' above if you would " +
' like to opt-in to cloud interaction for your Moblab.';
}
}
isCloudEnabledCheckboxChange() {
/**
* Invoked on change of cloud enabled checkbox.
*/
this.isCloudEnabled = !this.isCloudEnabled;
if (this.isCloudEnabled) {
this.isRemoteCommandEnabled = true;
this.isRemoteConsoleEnabled = true;
}
this.toggleCloudConfigDisabledMessage();
}
isRemoteConsoleEnabledCheckboxChange() {
/**
* Invoked on change of remote console enabled checkbox.
*/
this.isRemoteConsoleEnabled = !this.isRemoteConsoleEnabled;
}
isRemoteCommandEnabledCheckboxChange() {
/**
* Invoked on change of remote command enabled checkbox.
*/
this.isRemoteCommandEnabled = !this.isRemoteCommandEnabled;
}
ngAfterContentInit(): void {
this.changeDetector.detectChanges();
}
/** Checks if the Moblab has ran some jobs already,
* if so user can not change the boto key or gc bucket. */
initCanUpdateCloudConfigFlag() {
this.botoKeyIDForm.disable();
this.botoKeySecretForm.disable();
this.bucketURLForm.disable();
this.canUpdateCloudConfig = false;
this.moblabRpcService.getNumJobs((count: number) => {
this.canUpdateCloudConfig = count === 0;
if (this.canUpdateCloudConfig) {
this.botoKeyIDForm.enable();
this.botoKeySecretForm.enable();
this.bucketURLForm.enable();
}
});
}
onCloudConfigSubmit(formValue) {
if (!this.bucketURLForm.value.endsWith('/')) {
this.bucketURLForm.setValue(this.bucketURLForm.value + '/');
}
this.configurationLoading = true;
this.moblabRpcService.set_cloud_configuration(
(message: string) => {
this.notificationsService.notify(message);
this.getCloudConfiguration(true);
},
(errorMsg: string) => {
this.configurationLoading = false;
this.notificationsService.error(errorMsg);
},
this.isCloudEnabled ? this.botoKeyIDForm.value : '',
this.isCloudEnabled ? this.botoKeySecretForm.value : '',
this.isCloudEnabled ? this.bucketURLForm.value : '',
this.isCloudEnabled,
this.isRemoteConsoleEnabled,
this.isRemoteCommandEnabled
);
}
cloudCheckboxHoverStart() {
/**
* Invoked on mouseover of the enable cloud checkbox. Will associate error
* messages on cloud configuration forms that unchecking cloud integration
* will clear existing cloud config values ( error messages are set only if
* the cloud checkbox in enabled during mouseover and there is at least one
* non-empty cloud configuration ).
*/
if (
this.isCloudEnabled &&
(this.botoKeyIDForm.value ||
this.botoKeySecretForm.value ||
this.bucketURLForm.value)
) {
this.showUncheckWarning = true;
const clearError = {clearWarning: true};
this.botoKeyIDForm.setErrors(clearError);
this.botoKeySecretForm.setErrors(clearError);
this.bucketURLForm.setErrors(clearError);
}
}
cloudCheckboxHoverStop() {
/**
* Invoked on mouseoff of the enable cloud checkbox. Will clear the error
* set by cloudCheckboxHoverStart
*/
if (
this.botoKeyIDForm.value ||
this.botoKeySecretForm.value ||
this.bucketURLForm.value
) {
this.botoKeyIDForm.setErrors(null);
this.botoKeySecretForm.setErrors(null);
this.bucketURLForm.setErrors(null);
this.botoKeyIDForm.updateValueAndValidity();
this.botoKeySecretForm.updateValueAndValidity();
this.bucketURLForm.updateValueAndValidity();
}
}
updateCloudConfigurationForms(newCloudConfig: CloudConfiguration) {
/**
* Updates the form values of the cloud configurations. Meant to be called
* with values returned from RPC call to get cloud info.
*/
this.botoKeyIDForm.setValue(newCloudConfig.botoKeyID);
this.botoKeySecretForm.setValue(newCloudConfig.botoKeySecret);
this.bucketURLForm.setValue(newCloudConfig.bucketURL);
this.isCloudEnabled = newCloudConfig.isCloudEnabled;
this.isRemoteConsoleEnabled = newCloudConfig.isRemoteConsoleEnabled;
this.isRemoteCommandEnabled = newCloudConfig.isRemoteCommandEnabled;
this.toggleCloudConfigDisabledMessage();
this.lastFetchedCloudConfig = newCloudConfig;
this.configurationLoading = false;
}
public async pauseClick() {
this.pauseButton.setIsLoadingStatus(true);
try {
const pauseRequesters =
await this.moblabConfigurationRpcService.pauseHostScheduling();
this.settingsService.updateHostSchedulingStatus(pauseRequesters);
} catch (error) {
console.log('pauseHostScheduling call failed with:' + error);
this.notificationsService.error(
'Failed to pause host scheduling: ' + error
);
} finally {
this.pauseButton.setIsLoadingStatus(false);
}
}
public async resumeClick() {
this.resumeButton.setIsLoadingStatus(true);
try {
const pauseRequesters =
await this.moblabConfigurationRpcService.resumeHostScheduler();
this.settingsService.updateHostSchedulingStatus(pauseRequesters);
} catch (error) {
console.log('resumeHostScheduler call failed with:' + error);
this.notificationsService.error(
'Failed to resume host scheduling: ' + error
);
} finally {
this.resumeButton.setIsLoadingStatus(false);
}
}
async stageTestBuild() {
const dialogRef = this.dialog.open(StageBuildDialogComponent);
try {
const responseMessage = await dialogRef.afterClosed().toPromise();
if (responseMessage) {
this.notificationsService.notify(responseMessage);
}
} catch (error) {
this.notificationsService.error(error);
}
}
}