Add Firmware page

Adds ability to upload a fimware image by local workstation
or TFTP. Also adds ability to reboot BMC from the backup image.

- Add route definition, component view, and store for
  Firmware page
- Get ActiveSoftwareImage location at /redfish/v1/Managers/bmc
- Get backup by checking for an image id that is not the same as
  the active image /redfish/v1/UpdateService/FirmwareInventory
- Switch running firmware image by making PATCH request to
  /redfish/v1/Managers/bmc

Signed-off-by: Yoshie Muranaka <yoshiemuranaka@gmail.com>
Change-Id: I04450e5a170d374122908c4c0349ba3b6e93ed2c
diff --git a/src/assets/styles/bmc/custom/_card.scss b/src/assets/styles/bmc/custom/_card.scss
new file mode 100644
index 0000000..1272189
--- /dev/null
+++ b/src/assets/styles/bmc/custom/_card.scss
@@ -0,0 +1,5 @@
+.card {
+  .bg-success {
+    background-color: $success-light !important;
+  }
+}
\ No newline at end of file
diff --git a/src/assets/styles/bmc/custom/_index.scss b/src/assets/styles/bmc/custom/_index.scss
index 0c393c5..b67712b 100644
--- a/src/assets/styles/bmc/custom/_index.scss
+++ b/src/assets/styles/bmc/custom/_index.scss
@@ -6,6 +6,7 @@
 @import "./bootstrap-grid";
 @import "./buttons";
 @import "./calendar";
+@import "./card";
 @import "./dropdown";
 @import "./forms";
 @import "./modal";
diff --git a/src/components/AppNavigation/AppNavigation.vue b/src/components/AppNavigation/AppNavigation.vue
index 51b586c..5101d82 100644
--- a/src/components/AppNavigation/AppNavigation.vue
+++ b/src/components/AppNavigation/AppNavigation.vue
@@ -105,7 +105,7 @@
                 {{ $t('appNavigation.dateTimeSettings') }}
               </b-nav-item>
               <b-nav-item
-                href="javascript:void(0)"
+                to="/configuration/firmware"
                 data-test-id="nav-container-firmware"
               >
                 {{ $t('appNavigation.firmware') }}
diff --git a/src/locales/en-US.json b/src/locales/en-US.json
index fa00c86..5ad6b78 100644
--- a/src/locales/en-US.json
+++ b/src/locales/en-US.json
@@ -173,6 +173,69 @@
       "successDelete": "Successfully deleted %{count} log. | Successfully deleted %{count} logs."
     }
   },
+  "pageFirmware": {
+    "backup": "Backup:",
+    "backupImage": "Backup image",
+    "bmcStatus": "BMC status",
+    "changeAndRebootBmc": "Change image and reboot BMC",
+    "changeToBackupImage": "Change to backup image",
+    "current": "Current:",
+    "firmwareOnSystem": "Firmware on system",
+    "hostStatus": "Host status",
+    "pageDescription": "Update firmware by uploading a system image file from your workstation or TFTP server",
+    "running": "Running",
+    "state": "State",
+    "updateCode": "Update code",
+    "alert": {
+      "operationInProgress": "Server power operation in progress.",
+      "serverShutdownRequiredBeforeUpdate": "Server shutdown required before update",
+      "serverShutdownRequiredInfo": "Shutdown will be orderly - OS will shutdown before the server shuts down.",
+      "shutDownServer": "Shut down server",
+      "updateProcess": "Update process",
+      "updateProcessInfo": "The new image will be uploaded and activated. After that, the BMC will reboot automatically to run from the new image."
+    },
+    "form": {
+      "imageFile": "Image file",
+      "imageFileName": "Image file name",
+      "onlyTarFilesAccepted": "Only .tar files accepted",
+      "tftpServer": "TFTP server",
+      "tftpServerIpAddress": "TFTP server IP address",
+      "uploadAndRebootBmc": "Upload and reboot BMC",
+      "uploadLocation": "Upload location",
+      "workstation": "Workstation"
+    },
+    "modal": {
+      "connectionToBmcWillBeLost": "Connection to BMC will be lost",
+      "serverShutdownMessage": "There will be a server outage until the server is powered back on. Are you sure you want to shut down?",
+      "serverShutdownWillCauseOutage": "Server shutdown will cause outage",
+      "shutDownServer": "Shut down server",
+      "rebootFromBackup": {
+        "message1": "A BMC reboot is required before the system can run the backup image %{backup}. The reboot will cause a disconnection, and may require logging in again.",
+        "message2": "The current firmware image %{current} will be moved to backup. During the reboot, server cannot be powered back on.",
+        "message3": "Are you sure you want to reboot the BMC from backup image %{backup}?",
+        "primaryAction": "Reboot BMC from backup image",
+        "title": "@:pageFirmware.modal.connectionToBmcWillBeLost"
+      },
+      "uploadAndReboot": {
+        "message1": "A BMC reboot is required before the system can run the new firmware image. The reboot will cause a disconnection, and may require logging in again.",
+        "message2": "During the reboot, the server cannot be powered back on. The backup image will be permanently deleted.",
+        "message3": "Are you sure you want to upload the new firmware image and reboot the BMC?",
+        "primaryAction": "Upload and reboot BMC",
+        "title": "@:pageFirmware.modal.connectionToBmcWillBeLost"
+      }
+    },
+    "toast": {
+      "errorRebootFromBackup": "Error rebooting from backup image.",
+      "errorUploadAndReboot": "Error uploading image.",
+      "infoRefreshApplicationMessage": "Refresh the application to confirm the code update has completed and was successful.",
+      "infoRefreshApplicationTitle": "Verify code update",
+      "infoUploadStartTimeMessage": "Start time: %{startTime}",
+      "infoUploadStartTimeTitle": "Upload started",
+      "successRebootFromBackup": "Successfully started reboot from backup image.",
+      "successUploadMessage": "The upload was successful. During code update, the BMC will be not be responsive. Wait for the code update notification before making any changes.",
+      "successUploadTitle": "Code update started"
+    }
+  },
   "pageHardwareStatus": {
     "dimmSlot": "DIMM slot",
     "fans": "Fans",
diff --git a/src/main.js b/src/main.js
index 8336cb3..497c751 100644
--- a/src/main.js
+++ b/src/main.js
@@ -7,6 +7,7 @@
   BadgePlugin,
   ButtonPlugin,
   BVConfigPlugin,
+  CardPlugin,
   CollapsePlugin,
   DropdownPlugin,
   FormPlugin,
@@ -94,6 +95,7 @@
     variant: 'primary'
   }
 });
+Vue.use(CardPlugin);
 Vue.use(CollapsePlugin);
 Vue.use(DropdownPlugin);
 Vue.use(FormPlugin);
diff --git a/src/router/index.js b/src/router/index.js
index eace2bb..f3b8d8a 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -90,6 +90,14 @@
         }
       },
       {
+        path: '/configuration/firmware',
+        name: 'firmware',
+        component: () => import('@/views/Configuration/Firmware'),
+        meta: {
+          title: 'appPageTitle.firmware'
+        }
+      },
+      {
         path: '/control/kvm',
         name: 'kvm',
         component: () => import('@/views/Control/Kvm'),
diff --git a/src/store/modules/Configuration/FirmwareStore.js b/src/store/modules/Configuration/FirmwareStore.js
index 5ec9173..ead5199 100644
--- a/src/store/modules/Configuration/FirmwareStore.js
+++ b/src/store/modules/Configuration/FirmwareStore.js
@@ -1,27 +1,158 @@
-import api from '../../api';
+import api from '@/store/api';
+import i18n from '@/i18n';
 
 const FirmwareStore = {
   namespaced: true,
   state: {
-    bmcFirmwareVersion: '--'
+    activeFirmware: {
+      version: '--',
+      id: null,
+      location: null
+    },
+    backupFirmware: {
+      version: '--',
+      id: null,
+      location: null,
+      status: '--'
+    },
+    applyTime: null
   },
   getters: {
-    bmcFirmwareVersion: state => state.bmcFirmwareVersion
+    systemFirmwareVersion: state => state.activeFirmware.version,
+    backupFirmwareVersion: state => state.backupFirmware.version,
+    backupFirmwareStatus: state => state.backupFirmware.status,
+    isRebootFromBackupAvailable: state =>
+      state.backupFirmware.id ? true : false
   },
   mutations: {
-    setBmcFirmwareVersion: (state, bmcFirmwareVersion) =>
-      (state.bmcFirmwareVersion = bmcFirmwareVersion)
+    setActiveFirmware: (state, { version, id, location }) => {
+      state.activeFirmware.version = version;
+      state.activeFirmware.id = id;
+      state.activeFirmware.location = location;
+    },
+    setBackupFirmware: (state, { version, id, location, status }) => {
+      state.backupFirmware.version = version;
+      state.backupFirmware.id = id;
+      state.backupFirmware.location = location;
+      state.backupFirmware.status = status;
+    },
+    setApplyTime: (state, applyTime) => (state.applyTime = applyTime)
   },
   actions: {
-    async getBmcFirmware({ commit }) {
+    async getSystemFirwareVersion({ commit, state }) {
       return await api
         .get('/redfish/v1/Managers/bmc')
-        .then(response => {
-          const bmcFirmwareVersion = response.data.FirmwareVersion;
-          commit('setBmcFirmwareVersion', bmcFirmwareVersion);
+        .then(({ data: { Links: { ActiveSoftwareImage } } }) => {
+          const location = ActiveSoftwareImage['@odata.id'];
+          return api.get(location);
         })
+        .then(({ data }) => {
+          const version = data.Version;
+          const id = data.Id;
+          const location = data['@odata.id'];
+          commit('setActiveFirmware', { version, id, location });
+          // TODO: temporary workaround to get 'Backup' Firmware
+          // information
+          return api.get('/redfish/v1/UpdateService/FirmwareInventory');
+        })
+        .then(({ data: { Members } }) => {
+          // TODO: temporary workaround to get 'Backup' Firmware
+          // information
+          // Check FirmwareInventory list for not ActiveSoftwareImage id
+          const backupLocation = Members.map(item => item['@odata.id']).find(
+            location => {
+              const id = location.split('/').pop();
+              return id !== state.activeFirmware.id;
+            }
+          );
+          if (backupLocation) {
+            return api.get(backupLocation);
+          }
+        })
+        .then(({ data } = {}) => {
+          if (!data) return;
+          const version = data.Version;
+          const id = data.Id;
+          const location = data['@odata.id'];
+          const status = data.Status ? data.Status.State : '--';
+          commit('setBackupFirmware', { version, id, location, status });
+        })
+        .catch(error => console.log(error));
+    },
+    getUpdateServiceApplyTime({ commit }) {
+      api
+        .get('/redfish/v1/UpdateService')
+        .then(({ data }) => {
+          const applyTime =
+            data.HttpPushUriOptions.HttpPushUriApplyTime.ApplyTime;
+          commit('setApplyTime', applyTime);
+        })
+        .catch(error => console.log(error));
+    },
+    setApplyTimeImmediate({ commit }) {
+      const data = {
+        HttpPushUriOptions: {
+          HttpPushUriApplyTime: {
+            ApplyTime: 'Immediate'
+          }
+        }
+      };
+      return api
+        .patch('/redfish/v1/UpdateService', data)
+        .then(() => commit('setApplyTime', 'Immediate'))
+        .catch(error => console.log(error));
+    },
+    async uploadFirmware({ state, dispatch }, image) {
+      if (state.applyTime !== 'Immediate') {
+        // ApplyTime must be set to Immediate before making
+        // request to update firmware
+        await dispatch('setApplyTimeImmediate');
+      }
+      return await api
+        .post('/redfish/v1/UpdateService', image, {
+          headers: { 'Content-Type': 'application/octet-stream' }
+        })
+        .then(() => dispatch('getSystemFirwareVersion'))
+        .then(() => i18n.t('pageFirmware.toast.successUploadMessage'))
         .catch(error => {
           console.log(error);
+          throw new Error(i18n.t('pageFirmware.toast.errorUploadAndReboot'));
+        });
+    },
+    async uploadFirmwareTFTP({ state, dispatch }, { address, filename }) {
+      const data = {
+        TransferProtocol: 'TFTP',
+        ImageURI: `${address}/${filename}`
+      };
+      if (state.applyTime !== 'Immediate') {
+        // ApplyTime must be set to Immediate before making
+        // request to update firmware
+        await dispatch('setApplyTimeImmediate');
+      }
+      return await api
+        .post('/redfish/v1/UpdateService', data)
+        .then(() => dispatch('getSystemFirwareVersion'))
+        .then(() => i18n.t('pageFirmware.toast.successUploadMessage'))
+        .catch(error => {
+          console.log(error);
+          throw new Error(i18n.t('pageFirmware.toast.errorUploadAndReboot'));
+        });
+    },
+    async switchFirmwareAndReboot({ state }) {
+      const backupLoaction = state.backupFirmware.location;
+      const data = {
+        Links: {
+          ActiveSoftwareImage: {
+            '@odata.id': backupLoaction
+          }
+        }
+      };
+      return await api
+        .patch('/redfish/v1/Managers/bmc', data)
+        .then(() => i18n.t('pageFirmware.toast.successRebootFromBackup'))
+        .catch(error => {
+          console.log(error);
+          throw new Error(i18n.t('pageFirmware.toast.errorRebootFromBackup'));
         });
     }
   }
diff --git a/src/views/Configuration/Firmware/Firmware.vue b/src/views/Configuration/Firmware/Firmware.vue
new file mode 100644
index 0000000..248b0ab
--- /dev/null
+++ b/src/views/Configuration/Firmware/Firmware.vue
@@ -0,0 +1,401 @@
+<template>
+  <b-container fluid="xl">
+    <page-title :description="$t('pageFirmware.pageDescription')" />
+    <!-- Operation in progress alert -->
+    <alert v-if="isOperationInProgress" variant="info" class="mb-5">
+      <p>
+        {{ $t('pageFirmware.alert.operationInProgress') }}
+      </p>
+    </alert>
+    <!-- Shutdown server warning alert -->
+    <alert v-else-if="!isHostOff" variant="warning" class="mb-5">
+      <p class="font-weight-bold mb-1">
+        {{ $t('pageFirmware.alert.serverShutdownRequiredBeforeUpdate') }}
+      </p>
+      {{ $t('pageFirmware.alert.serverShutdownRequiredInfo') }}
+      <template v-slot:action>
+        <b-btn variant="link" class="text-nowrap" @click="onClickShutDown">
+          {{ $t('pageFirmware.alert.shutDownServer') }}
+        </b-btn>
+      </template>
+    </alert>
+    <b-row class="mb-4">
+      <!-- Firmware on system -->
+      <b-col md="10" lg="12" xl="8" class="pr-xl-4">
+        <page-section :section-title="$t('pageFirmware.firmwareOnSystem')">
+          <b-card-group deck>
+            <!-- Current FW -->
+            <b-card header-bg-variant="success">
+              <template v-slot:header>
+                <dl class="mb-0">
+                  <dt>{{ $t('pageFirmware.current') }}</dt>
+                  <dd class="mb-0">{{ systemFirmwareVersion }}</dd>
+                </dl>
+              </template>
+              <b-row>
+                <b-col xs="6">
+                  <dl class="my-0">
+                    <dt>{{ $t('pageFirmware.bmcStatus') }}</dt>
+                    <dd>{{ $t('pageFirmware.running') }}</dd>
+                  </dl>
+                </b-col>
+                <b-col xs="6">
+                  <dl class="my-0">
+                    <dt>{{ $t('pageFirmware.hostStatus') }}</dt>
+                    <dd v-if="hostStatus === 'on'">
+                      {{ $t('global.status.on') }}
+                    </dd>
+                    <dd v-else-if="hostStatus === 'off'">
+                      {{ $t('global.status.off') }}
+                    </dd>
+                    <dd v-else>
+                      {{ $t('global.status.notAvailable') }}
+                    </dd>
+                  </dl>
+                </b-col>
+              </b-row>
+            </b-card>
+
+            <!-- Backup FW -->
+            <b-card>
+              <template v-slot:header>
+                <dl class="mb-0">
+                  <dt>{{ $t('pageFirmware.backup') }}</dt>
+                  <dd class="mb-0">{{ backupFirmwareVersion }}</dd>
+                </dl>
+              </template>
+              <b-row>
+                <b-col xs="6">
+                  <dl class="my-0">
+                    <dt>{{ $t('pageFirmware.state') }}</dt>
+                    <dd>{{ backupFirmwareStatus }}</dd>
+                  </dl>
+                </b-col>
+              </b-row>
+            </b-card>
+          </b-card-group>
+        </page-section>
+
+        <!-- Change to backup image -->
+        <page-section :section-title="$t('pageFirmware.changeToBackupImage')">
+          <dl class="mb-5">
+            <dt>
+              {{ $t('pageFirmware.backupImage') }}
+            </dt>
+            <dd>{{ backupFirmwareVersion }}</dd>
+          </dl>
+          <b-btn
+            v-b-modal.modal-reboot-backup
+            type="button"
+            variant="primary"
+            :disabled="isPageDisabled || !isRebootFromBackupAvailable"
+          >
+            {{ $t('pageFirmware.changeAndRebootBmc') }}
+          </b-btn>
+        </page-section>
+      </b-col>
+
+      <!-- Update code -->
+      <b-col sm="8" xl="4" class="update-code pl-xl-4">
+        <page-section :section-title="$t('pageFirmware.updateCode')">
+          <b-form @submit.prevent="onSubmitUpload">
+            <b-form-group
+              :label="$t('pageFirmware.form.uploadLocation')"
+              :disabled="isPageDisabled"
+            >
+              <b-form-radio v-model="isWorkstationSelected" :value="true">
+                {{ $t('pageFirmware.form.workstation') }}
+              </b-form-radio>
+              <b-form-radio v-model="isWorkstationSelected" :value="false">
+                {{ $t('pageFirmware.form.tftpServer') }}
+              </b-form-radio>
+            </b-form-group>
+
+            <!-- Workstation Upload -->
+            <template v-if="isWorkstationSelected">
+              <b-form-group
+                :label="$t('pageFirmware.form.imageFile')"
+                label-for="image-file"
+              >
+                <b-form-text id="image-file-help-block">
+                  {{ $t('pageFirmware.form.onlyTarFilesAccepted') }}
+                </b-form-text>
+                <b-form-file
+                  id="image-file"
+                  v-model="file"
+                  accept=".tar"
+                  aria-describedby="image-file-help-block"
+                  :disabled="isPageDisabled"
+                  :state="getValidationState($v.file)"
+                  @input="$v.file.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  {{ $t('global.form.required') }}
+                </b-form-invalid-feedback>
+              </b-form-group>
+            </template>
+
+            <!-- TFTP Server Upload -->
+            <template v-else>
+              <b-form-group
+                :label="$t('pageFirmware.form.tftpServerIpAddress')"
+                label-for="tftp-ip"
+              >
+                <b-form-input
+                  id="tftp-id"
+                  v-model="tftpIpAddress"
+                  type="text"
+                  :browse-text="$t('global.fileUpload.browseText')"
+                  :drop-placeholder="$t('global.fileUpload.dropPlaceholder')"
+                  :placeholder="$t('global.fileUpload.placeholder')"
+                  :state="getValidationState($v.tftpIpAddress)"
+                  :disabled="isPageDisabled"
+                  @input="$v.tftpIpAddress.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  {{ $t('global.form.fieldRequired') }}
+                </b-form-invalid-feedback>
+              </b-form-group>
+              <b-form-group
+                :label="$t('pageFirmware.form.imageFileName')"
+                label-for="tftp-file-name"
+              >
+                <b-form-input
+                  id="tftp-file-name"
+                  v-model="tftpFileName"
+                  type="text"
+                  :state="getValidationState($v.tftpFileName)"
+                  :disabled="isPageDisabled"
+                  @input="$v.tftpFileName.$touch()"
+                />
+                <b-form-invalid-feedback role="alert">
+                  {{ $t('global.form.fieldRequired') }}
+                </b-form-invalid-feedback>
+              </b-form-group>
+            </template>
+
+            <!-- Info alert -->
+            <alert variant="info" class="mt-4 mb-5">
+              <p class="font-weight-bold mb-1">
+                {{ $t('pageFirmware.alert.updateProcess') }}
+              </p>
+              <p>{{ $t('pageFirmware.alert.updateProcessInfo') }}</p>
+            </alert>
+            <b-form-group>
+              <b-btn type="submit" variant="primary" :disabled="isPageDisabled">
+                {{ $t('pageFirmware.form.uploadAndRebootBmc') }}
+              </b-btn>
+            </b-form-group>
+          </b-form>
+        </page-section>
+      </b-col>
+    </b-row>
+
+    <!-- Modals -->
+    <modal-upload @ok="uploadFirmware" />
+    <modal-reboot-backup
+      :current="currentFirmwareVersion"
+      :backup="backupFirmwareVersion"
+      @ok="rebootFromBackup"
+    />
+  </b-container>
+</template>
+
+<script>
+import { requiredIf } from 'vuelidate/lib/validators';
+import { mapGetters } from 'vuex';
+
+import PageSection from '@/components/Global/PageSection';
+import PageTitle from '@/components/Global/PageTitle';
+import Alert from '@/components/Global/Alert';
+import ModalUpload from './FirmwareModalUpload';
+import ModalRebootBackup from './FirmwareModalRebootBackup';
+
+import VuelidateMixin from '@/components/Mixins/VuelidateMixin.js';
+import LoadingBarMixin from '@/components/Mixins/LoadingBarMixin';
+import BVToastMixin from '@/components/Mixins/BVToastMixin';
+
+export default {
+  name: 'Firmware',
+  components: {
+    Alert,
+    ModalRebootBackup,
+    ModalUpload,
+    PageSection,
+    PageTitle
+  },
+  mixins: [BVToastMixin, LoadingBarMixin, VuelidateMixin],
+  data() {
+    return {
+      isWorkstationSelected: true,
+      file: null,
+      tftpIpAddress: null,
+      tftpFileName: null,
+      timeoutId: null
+    };
+  },
+  computed: {
+    hostStatus() {
+      return this.$store.getters['global/hostStatus'];
+    },
+    isHostOff() {
+      return this.hostStatus === 'off' ? true : false;
+    },
+    isOperationInProgress() {
+      return this.$store.getters['controls/isOperationInProgress'];
+    },
+    ...mapGetters('firmware', [
+      'backupFirmwareStatus',
+      'backupFirmwareVersion',
+      'isRebootFromBackupAvailable',
+      'systemFirmwareVersion'
+    ]),
+    isPageDisabled() {
+      return !this.isHostOff || this.loading || this.isOperationInProgress;
+    }
+  },
+  watch: {
+    isWorkstationSelected: function() {
+      this.$v.$reset();
+      this.file = null;
+      this.tftpIpAddress = null;
+      this.tftpFileName = null;
+    }
+  },
+  created() {
+    this.startLoader();
+    this.$store.dispatch('firmware/getUpdateServiceApplyTime');
+    Promise.all([
+      this.$store.dispatch('global/getHostStatus'),
+      this.$store.dispatch('firmware/getSystemFirwareVersion')
+    ]).finally(() => this.endLoader());
+  },
+  beforeRouteLeave(to, from, next) {
+    this.hideLoader();
+    this.clearRebootTimeout();
+    next();
+  },
+  validations() {
+    return {
+      file: {
+        required: requiredIf(function() {
+          return this.isWorkstationSelected;
+        })
+      },
+      tftpIpAddress: {
+        required: requiredIf(function() {
+          return !this.isWorkstationSelected;
+        })
+      },
+      tftpFileName: {
+        required: requiredIf(function() {
+          return !this.isWorkstationSelected;
+        })
+      }
+    };
+  },
+  methods: {
+    uploadFirmware() {
+      const startTime = this.$options.filters.formatTime(new Date());
+      this.setRebootTimeout(360000); //6 minute timeout
+      this.infoToast(
+        this.$t('pageFirmware.toast.infoUploadStartTimeMessage', { startTime }),
+        this.$t('pageFirmware.toast.infoUploadStartTimeTitle')
+      );
+      if (this.isWorkstationSelected) {
+        this.dispatchWorkstationUpload();
+      } else {
+        this.dispatchTftpUpload();
+      }
+    },
+    dispatchWorkstationUpload() {
+      this.$store
+        .dispatch('firmware/uploadFirmware', this.file)
+        .then(success =>
+          this.infoToast(
+            success,
+            this.$t('pageFirmware.toast.successUploadTitle')
+          )
+        )
+        .catch(({ message }) => {
+          this.errorToast(message);
+          this.clearRebootTimeout();
+        });
+    },
+    dispatchTftpUpload() {
+      const data = {
+        address: this.tftpIpAddress,
+        filename: this.tftpFileName
+      };
+      this.$store
+        .dispatch('firmware/uploadFirmwareTFTP', data)
+        .then(success =>
+          this.infoToast(
+            success,
+            this.$t('pageFirmware.toast.successUploadTitle')
+          )
+        )
+        .catch(({ message }) => {
+          this.errorToast(message);
+          this.clearRebootTimeout();
+        });
+    },
+    rebootFromBackup() {
+      this.setRebootTimeout();
+      this.$store
+        .dispatch('firmware/switchFirmwareAndReboot')
+        .then(success =>
+          this.infoToast(success, this.$t('global.status.success'))
+        )
+        .catch(({ message }) => {
+          this.errorToast(message);
+          this.clearRebootTimeout();
+        });
+    },
+    setRebootTimeout(timeoutMs = 60000) {
+      // Set a timeout to disable page interactions while
+      // an upload or BMC reboot is in progress
+      this.startLoader();
+      this.timeoutId = setTimeout(() => {
+        this.endLoader();
+        this.infoToast(
+          this.$t('pageFirmware.toast.infoRefreshApplicationMessage'),
+          this.$t('pageFirmware.toast.infoRefreshApplicationTitle')
+        );
+      }, timeoutMs);
+    },
+    clearRebootTimeout() {
+      if (this.timeoutId) {
+        clearTimeout(this.timeoutId);
+        this.endLoader();
+      }
+    },
+    onSubmitUpload() {
+      this.$v.$touch();
+      if (this.$v.$invalid) return;
+      this.$bvModal.show('modal-upload');
+    },
+    onClickShutDown() {
+      this.$bvModal
+        .msgBoxConfirm(this.$t('pageFirmware.modal.serverShutdownMessage'), {
+          title: this.$t('pageFirmware.modal.serverShutdownWillCauseOutage'),
+          okTitle: this.$t('pageFirmware.modal.shutDownServer'),
+          okVariant: 'danger'
+        })
+        .then(shutdownConfirmed => {
+          if (shutdownConfirmed)
+            this.$store.dispatch('controls/hostSoftPowerOff');
+        });
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.update-code {
+  border-left: none;
+  @include media-breakpoint-up(xl) {
+    border-left: 1px solid gray('300');
+  }
+}
+</style>
diff --git a/src/views/Configuration/Firmware/FirmwareModalRebootBackup.vue b/src/views/Configuration/Firmware/FirmwareModalRebootBackup.vue
new file mode 100644
index 0000000..a8fb3ad
--- /dev/null
+++ b/src/views/Configuration/Firmware/FirmwareModalRebootBackup.vue
@@ -0,0 +1,33 @@
+<template>
+  <b-modal
+    id="modal-reboot-backup"
+    :ok-title="$t('pageFirmware.modal.rebootFromBackup.primaryAction')"
+    :title="$t('pageFirmware.modal.rebootFromBackup.title')"
+    @ok="$emit('ok')"
+  >
+    <p>
+      {{ $t('pageFirmware.modal.rebootFromBackup.message1', { backup }) }}
+    </p>
+    <p>
+      {{ $t('pageFirmware.modal.rebootFromBackup.message2', { current }) }}
+    </p>
+    <p class="font-weight-bold">
+      {{ $t('pageFirmware.modal.rebootFromBackup.message3', { backup }) }}
+    </p>
+  </b-modal>
+</template>
+
+<script>
+export default {
+  props: {
+    current: {
+      type: String,
+      required: true
+    },
+    backup: {
+      type: String,
+      required: true
+    }
+  }
+};
+</script>
diff --git a/src/views/Configuration/Firmware/FirmwareModalUpload.vue b/src/views/Configuration/Firmware/FirmwareModalUpload.vue
new file mode 100644
index 0000000..d092bec
--- /dev/null
+++ b/src/views/Configuration/Firmware/FirmwareModalUpload.vue
@@ -0,0 +1,18 @@
+<template>
+  <b-modal
+    id="modal-upload"
+    :title="$t('pageFirmware.modal.uploadAndReboot.title')"
+    :ok-title="$t('pageFirmware.modal.uploadAndReboot.primaryAction')"
+    @ok="$emit('ok')"
+  >
+    <p>
+      {{ $t('pageFirmware.modal.uploadAndReboot.message1') }}
+    </p>
+    <p>
+      {{ $t('pageFirmware.modal.uploadAndReboot.message2') }}
+    </p>
+    <p class="font-weight-bold">
+      {{ $t('pageFirmware.modal.uploadAndReboot.message3') }}
+    </p>
+  </b-modal>
+</template>
diff --git a/src/views/Configuration/Firmware/index.js b/src/views/Configuration/Firmware/index.js
new file mode 100644
index 0000000..ad15cc0
--- /dev/null
+++ b/src/views/Configuration/Firmware/index.js
@@ -0,0 +1,2 @@
+import Firmware from './Firmware.vue';
+export default Firmware;
diff --git a/src/views/Overview/Overview.vue b/src/views/Overview/Overview.vue
index 46944cc..ac48481 100644
--- a/src/views/Overview/Overview.vue
+++ b/src/views/Overview/Overview.vue
@@ -106,7 +106,7 @@
   mixins: [LoadingBarMixin],
   computed: mapState({
     server: state => state.system.systems[0],
-    bmcFirmwareVersion: state => state.firmware.bmcFirmwareVersion,
+    bmcFirmwareVersion: state => state.firmware.activeFirmware.version,
     powerCapValue: state => state.powerControl.powerCapValue,
     powerConsumptionValue: state => state.powerControl.powerConsumptionValue,
     serverManufacturer() {
@@ -139,7 +139,7 @@
     });
     Promise.all([
       this.$store.dispatch('system/getSystem'),
-      this.$store.dispatch('firmware/getBmcFirmware'),
+      this.$store.dispatch('firmware/getSystemFirwareVersion'),
       this.$store.dispatch('powerControl/getPowerControl'),
       quicklinksPromise,
       networkPromise,