Add host boot settings to power operations page

Added BootSettingsStore and component to handle changing boot
source, boot override option and TPM required option.

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I885dd6008aceb34b319953a2e9b6416d848baf16
diff --git a/src/assets/styles/_form-components.scss b/src/assets/styles/_form-components.scss
index 89abfb3..35274e7 100644
--- a/src/assets/styles/_form-components.scss
+++ b/src/assets/styles/_form-components.scss
@@ -25,3 +25,13 @@
     border-bottom: 2px solid $danger !important;
   }
 }
+
+.custom-control {
+  .custom-control-input[disabled=disabled] {
+    & + .custom-control-label {
+      // Disabled label for checkbox, radio,
+      // switch bootstrap form components
+      color: $gray-700!important;
+    }
+  }
+}
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index 8a77b93..0bf4051 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -183,9 +183,11 @@
   "pageServerPowerOperations": {
     "currentStatus": "Current status",
     "hostname": "Hostname",
+    "hostOsBootSettings": "Host OS boot settings",
     "hostStatus": "Host status",
     "immediateReboot": "Immediate – Server reboots without OS shutting down; may cause data corruption",
     "immediateShutdown": "Immediate - Server shuts down without OS shutting down; may cause data corruption",
+    "oneTimeBootWarning": "Pending one time boot. Next boot will be performed with the specified one time boot settings. Subsequent boots will be performed with the default settings.",
     "operationInProgress": "There are no options to display while a power operation is in progress. When complete, power operations will be displayed here.",
     "operations": "Operations",
     "orderlyReboot": "Orderly – OS shuts down, then server reboots",
@@ -195,11 +197,21 @@
     "rebootServer": "Reboot server",
     "shutDown": "Shut down",
     "shutdownServer": "Shutdown server",
+    "bootSettings": {
+      "bootSettingsOverride": "Boot settings override",
+      "enableOneTimeBoot": "Enable one time boot",
+      "tpmRequiredPolicy": "TPM required policy",
+      "tpmRequiredPolicyHelper": "Enable to ensure the system only boots when the TPM is functional."
+    },
     "modal": {
       "confirmRebootMessage": "Are you sure you want to reboot?",
       "confirmRebootTitle": "Server reboot will cause outage",
       "confirmShutdownMessage": "Are you sure you want to shut down?",
       "confirmShutdownTitle": "Server shutdown will cause outage"
+    },
+    "toast": {
+      "errorSaveSettings": "Error saving settings.",
+      "successSaveSettings": "Successfully saved settings."
     }
   }
 }
\ No newline at end of file
diff --git a/src/store/index.js b/src/store/index.js
index 6bad517..2721699 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -6,6 +6,7 @@
 import LocalUserManagementStore from './modules/AccessControl/LocalUserMangementStore';
 import OverviewStore from './modules/Overview/OverviewStore';
 import FirmwareStore from './modules/Configuration/FirmwareStore';
+import BootSettingsStore from './modules/Control/BootSettingsStore';
 import ControlStore from './modules/Control/ControlStore';
 import PowerControlStore from './modules/Control/PowerControlStore';
 import NetworkSettingStore from './modules/Configuration/NetworkSettingsStore';
@@ -25,6 +26,7 @@
     localUsers: LocalUserManagementStore,
     overview: OverviewStore,
     firmware: FirmwareStore,
+    hostBootSettings: BootSettingsStore,
     controls: ControlStore,
     powerControl: PowerControlStore,
     networkSettings: NetworkSettingStore,
diff --git a/src/store/modules/Control/BootSettingsStore.js b/src/store/modules/Control/BootSettingsStore.js
new file mode 100644
index 0000000..8da586a
--- /dev/null
+++ b/src/store/modules/Control/BootSettingsStore.js
@@ -0,0 +1,136 @@
+import api from '../../api';
+import i18n from '../../../i18n';
+
+const BootSettingsStore = {
+  namespaced: true,
+  state: {
+    bootSourceOptions: [],
+    bootSource: null,
+    overrideEnabled: null,
+    tpmEnabled: null
+  },
+  getters: {
+    bootSourceOptions: state => state.bootSourceOptions,
+    bootSource: state => state.bootSource,
+    overrideEnabled: state => state.overrideEnabled,
+    tpmEnabled: state => state.tpmEnabled
+  },
+  mutations: {
+    setBootSourceOptions: (state, bootSourceOptions) =>
+      (state.bootSourceOptions = bootSourceOptions),
+    setBootSource: (state, bootSource) => (state.bootSource = bootSource),
+    setOverrideEnabled: (state, overrideEnabled) => {
+      if (overrideEnabled === 'Once') {
+        state.overrideEnabled = true;
+      } else {
+        // 'Continuous' or 'Disabled'
+        state.overrideEnabled = false;
+      }
+    },
+    setTpmPolicy: (state, tpmEnabled) => (state.tpmEnabled = tpmEnabled)
+  },
+  actions: {
+    getBootSettings({ commit }) {
+      api
+        .get('/redfish/v1/Systems/system/')
+        .then(({ data: { Boot } }) => {
+          commit(
+            'setBootSourceOptions',
+            Boot['BootSourceOverrideTarget@Redfish.AllowableValues']
+          );
+          commit('setOverrideEnabled', Boot.BootSourceOverrideEnabled);
+          commit('setBootSource', Boot.BootSourceOverrideTarget);
+        })
+        .catch(error => console.log(error));
+    },
+    saveBootSettings({ commit, dispatch }, { bootSource, overrideEnabled }) {
+      const data = { Boot: {} };
+      data.Boot.BootSourceOverrideTarget = bootSource;
+
+      if (overrideEnabled) {
+        data.Boot.BootSourceOverrideEnabled = 'Once';
+      } else if (bootSource === 'None') {
+        data.Boot.BootSourceOverrideEnabled = 'Disabled';
+      } else {
+        data.Boot.BootSourceOverrideEnabled = 'Continuous';
+      }
+
+      return api
+        .patch('/redfish/v1/Systems/system', data)
+        .then(response => {
+          // If request success, commit the values
+          commit('setBootSource', data.Boot.BootSourceOverrideTarget);
+          commit('setOverrideEnabled', data.Boot.BootSourceOverrideEnabled);
+          return response;
+        })
+        .catch(error => {
+          console.log(error);
+          // If request error, GET saved options
+          dispatch('getBootSettings');
+          return error;
+        });
+    },
+    getTpmPolicy({ commit }) {
+      // TODO: switch to Redfish when available
+      api
+        .get('/xyz/openbmc_project/control/host0/TPMEnable')
+        .then(({ data: { data: { TPMEnable } } }) =>
+          commit('setTpmPolicy', TPMEnable)
+        )
+        .catch(error => console.log(error));
+    },
+    saveTpmPolicy({ commit, dispatch }, tpmEnabled) {
+      // TODO: switch to Redfish when available
+      const data = { data: tpmEnabled };
+      return api
+        .put(
+          '/xyz/openbmc_project/control/host0/TPMEnable/attr/TPMEnable',
+          data
+        )
+        .then(response => {
+          // If request success, commit the values
+          commit('setTpmPolicy', tpmEnabled);
+          return response;
+        })
+        .catch(error => {
+          console.log(error);
+          // If request error, GET saved policy
+          dispatch('getTpmPolicy');
+          return error;
+        });
+    },
+    async saveSettings(
+      { dispatch },
+      { bootSource, overrideEnabled, tpmEnabled }
+    ) {
+      const promises = [];
+
+      if (bootSource !== null || overrideEnabled !== null) {
+        promises.push(
+          dispatch('saveBootSettings', { bootSource, overrideEnabled })
+        );
+      }
+      if (tpmEnabled !== null) {
+        promises.push(dispatch('saveTpmPolicy', tpmEnabled));
+      }
+
+      return await api.all(promises).then(
+        api.spread((...responses) => {
+          let message = i18n.t(
+            'pageServerPowerOperations.toast.successSaveSettings'
+          );
+          responses.forEach(response => {
+            if (response instanceof Error) {
+              throw new Error(
+                i18n.t('pageServerPowerOperations.toast.errorSaveSettings')
+              );
+            }
+          });
+          return message;
+        })
+      );
+    }
+  }
+};
+
+export default BootSettingsStore;
diff --git a/src/views/Control/ServerPowerOperations/BootSettings.vue b/src/views/Control/ServerPowerOperations/BootSettings.vue
new file mode 100644
index 0000000..c912749
--- /dev/null
+++ b/src/views/Control/ServerPowerOperations/BootSettings.vue
@@ -0,0 +1,148 @@
+<template>
+  <div class="boot-settings p-3">
+    <b-form novalidate @submit.prevent="handleSubmit">
+      <b-form-group
+        :label="
+          $t('pageServerPowerOperations.bootSettings.bootSettingsOverride')
+        "
+        label-for="boot-option"
+        class="mb-3"
+      >
+        <b-form-select
+          id="boot-option"
+          v-model="form.bootOption"
+          :disabled="bootSourceOptions.length === 0"
+          :options="bootSourceOptions"
+          @change="onChangeSelect"
+        >
+        </b-form-select>
+      </b-form-group>
+      <b-form-checkbox
+        v-model="form.oneTimeBoot"
+        class="mb-4"
+        :disabled="form.bootOption === 'None'"
+        @change="$v.form.oneTimeBoot.$touch()"
+      >
+        {{ $t('pageServerPowerOperations.bootSettings.enableOneTimeBoot') }}
+      </b-form-checkbox>
+      <b-form-group
+        :label="$t('pageServerPowerOperations.bootSettings.tpmRequiredPolicy')"
+      >
+        <b-form-text id="tpm-required-policy-help-block">
+          {{
+            $t('pageServerPowerOperations.bootSettings.tpmRequiredPolicyHelper')
+          }}
+        </b-form-text>
+        <b-form-checkbox
+          id="tpm-required-policy"
+          v-model="form.tpmPolicyOn"
+          switch
+          aria-describedby="tpm-required-policy-help-block"
+          @change="$v.form.tpmPolicyOn.$touch()"
+        >
+          {{
+            form.tpmPolicyOn ? $t('global.status.on') : $t('global.status.off')
+          }}
+        </b-form-checkbox>
+      </b-form-group>
+      <b-button
+        variant="primary"
+        type="submit"
+        class="mb-3"
+        :disabled="!$v.form.$anyDirty"
+      >
+        {{ $t('global.action.save') }}
+      </b-button>
+    </b-form>
+  </div>
+</template>
+
+<script>
+import { mapState } from 'vuex';
+import BVToastMixin from '../../../components/Mixins/BVToastMixin';
+
+export default {
+  name: 'BootSettings',
+  mixins: [BVToastMixin],
+  data() {
+    return {
+      form: {
+        bootOption: this.$store.getters['hostBootSettings/bootSource'],
+        oneTimeBoot: this.$store.getters['hostBootSettings/overrideEnabled'],
+        tpmPolicyOn: this.$store.getters['hostBootSettings/tpmEnabled']
+      }
+    };
+  },
+  computed: {
+    ...mapState('hostBootSettings', [
+      'bootSourceOptions',
+      'bootSource',
+      'overrideEnabled',
+      'tpmEnabled'
+    ])
+  },
+  watch: {
+    bootSource: function(value) {
+      this.form.bootOption = value;
+    },
+    overrideEnabled: function(value) {
+      this.form.oneTimeBoot = value;
+    },
+    tpmEnabled: function(value) {
+      this.form.tpmPolicyOn = value;
+    }
+  },
+  validations: {
+    // Empty validations to leverage vuelidate form states
+    // to check for changed values
+    form: {
+      bootOption: {},
+      oneTimeBoot: {},
+      tpmPolicyOn: {}
+    }
+  },
+  created() {
+    this.$store.dispatch('hostBootSettings/getBootSettings');
+    this.$store.dispatch('hostBootSettings/getTpmPolicy');
+  },
+  methods: {
+    handleSubmit() {
+      const bootSettingsChanged =
+        this.$v.form.bootOption.$dirty || this.$v.form.oneTimeBoot.$dirty;
+      const tpmPolicyChanged = this.$v.form.tpmPolicyOn.$dirty;
+      let settings;
+      let bootSource = null;
+      let overrideEnabled = null;
+      let tpmEnabled = null;
+
+      if (bootSettingsChanged) {
+        // If bootSource or overrideEnabled changed get
+        // both current values to send with request
+        bootSource = this.form.bootOption;
+        overrideEnabled = this.form.oneTimeBoot;
+      }
+      if (tpmPolicyChanged) tpmEnabled = this.form.tpmPolicyOn;
+      settings = { bootSource, overrideEnabled, tpmEnabled };
+
+      this.$store
+        .dispatch('hostBootSettings/saveSettings', settings)
+        .then(message => this.successToast(message))
+        .catch(({ message }) => this.errorToast(message))
+        .finally(() => {
+          this.$v.form.$reset();
+        });
+    },
+    onChangeSelect(selectedOption) {
+      this.$v.form.bootOption.$touch();
+      // Disable one time boot if selected boot option is 'None'
+      if (selectedOption === 'None') this.form.oneTimeBoot = false;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.boot-settings {
+  background-color: $gray-200;
+}
+</style>
diff --git a/src/views/Control/ServerPowerOperations/ServerPowerOperations.vue b/src/views/Control/ServerPowerOperations/ServerPowerOperations.vue
index c9b02b3..e63d073 100644
--- a/src/views/Control/ServerPowerOperations/ServerPowerOperations.vue
+++ b/src/views/Control/ServerPowerOperations/ServerPowerOperations.vue
@@ -2,7 +2,7 @@
   <b-container fluid>
     <page-title />
     <b-row>
-      <b-col md="8" lg="8" xl="6">
+      <b-col md="8" xl="6">
         <page-section
           :section-title="$t('pageServerPowerOperations.currentStatus')"
         >
@@ -26,10 +26,20 @@
       </b-col>
     </b-row>
     <b-row>
-      <b-col md="8" lg="7" xl="8">
+      <b-col sm="8" md="6" xl="4">
+        <page-section
+          :section-title="$t('pageServerPowerOperations.hostOsBootSettings')"
+        >
+          <boot-settings />
+        </page-section>
+      </b-col>
+      <b-col sm="8" md="6" xl="7">
         <page-section
           :section-title="$t('pageServerPowerOperations.operations')"
         >
+          <b-alert :show="oneTimeBootEnabled" variant="warning">
+            {{ $t('pageServerPowerOperations.oneTimeBootWarning') }}
+          </b-alert>
           <template v-if="isOperationInProgress">
             {{ $t('pageServerPowerOperations.operationInProgress') }}
           </template>
@@ -101,10 +111,11 @@
 import PageTitle from '../../../components/Global/PageTitle';
 import PageSection from '../../../components/Global/PageSection';
 import BVToastMixin from '../../../components/Mixins/BVToastMixin';
+import BootSettings from './BootSettings';
 
 export default {
   name: 'ServerPowerOperations',
-  components: { PageTitle, PageSection },
+  components: { PageTitle, PageSection, BootSettings },
   mixins: [BVToastMixin],
   data() {
     return {
@@ -123,6 +134,9 @@
     },
     isOperationInProgress() {
       return this.$store.getters['controls/isOperationInProgress'];
+    },
+    oneTimeBootEnabled() {
+      return this.$store.getters['hostBootSettings/overrideEnabled'];
     }
   },
   created() {